Improve pipeline editor UX: stage detail sheet, structured predicates, page reorganization
Build and Push Docker Image / build (push) Failing after 40s
Details
Build and Push Docker Image / build (push) Failing after 40s
Details
- Add Sheet UI component and StageDetailSheet with config/activity tabs - Stage config opens in right-side sheet (always-editable, no collapsed summary) - Replace JSON textarea in routing rules with structured PredicateBuilder form - Remove StageTransitionsEditor from UI (transitions auto-managed) - Promote Stage Management section to immediately after flowchart - Conditionally hide Routing Rules (single track) and Award Governance (no awards) - Add section headers with descriptions and increase spacing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
2d91ce02fc
commit
c321d4711e
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,196 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Code } from 'lucide-react'
|
||||
|
||||
const FIELD_OPTIONS = [
|
||||
{ value: 'competitionCategory', label: 'Competition Category' },
|
||||
{ value: 'oceanIssue', label: 'Ocean Issue' },
|
||||
{ value: 'country', label: 'Country' },
|
||||
{ value: 'geographicZone', label: 'Geographic Zone' },
|
||||
{ value: 'wantsMentorship', label: 'Wants Mentorship' },
|
||||
{ value: 'tags', label: 'Tags' },
|
||||
] as const
|
||||
|
||||
const OPERATOR_OPTIONS = [
|
||||
{ value: 'equals', label: 'equals' },
|
||||
{ value: 'not_equals', label: 'not equals' },
|
||||
{ value: 'contains', label: 'contains' },
|
||||
{ value: 'in', label: 'in' },
|
||||
] as const
|
||||
|
||||
type SimplePredicate = {
|
||||
field: string
|
||||
operator: string
|
||||
value: string
|
||||
}
|
||||
|
||||
type PredicateBuilderProps = {
|
||||
value: Record<string, unknown>
|
||||
onChange: (predicate: Record<string, unknown>) => void
|
||||
}
|
||||
|
||||
function isSimplePredicate(obj: Record<string, unknown>): obj is SimplePredicate {
|
||||
return (
|
||||
typeof obj.field === 'string' &&
|
||||
typeof obj.operator === 'string' &&
|
||||
(typeof obj.value === 'string' || typeof obj.value === 'boolean')
|
||||
)
|
||||
}
|
||||
|
||||
function isCompound(obj: Record<string, unknown>): boolean {
|
||||
return 'or' in obj || 'and' in obj || 'not' in obj
|
||||
}
|
||||
|
||||
export function PredicateBuilder({ value, onChange }: PredicateBuilderProps) {
|
||||
const [jsonMode, setJsonMode] = useState(false)
|
||||
const [jsonText, setJsonText] = useState('')
|
||||
|
||||
const compound = isCompound(value)
|
||||
const simple = !compound && isSimplePredicate(value)
|
||||
|
||||
useEffect(() => {
|
||||
if (compound) {
|
||||
setJsonMode(true)
|
||||
setJsonText(JSON.stringify(value, null, 2))
|
||||
}
|
||||
}, [compound, value])
|
||||
|
||||
if (jsonMode) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label className="text-xs">Predicate (JSON)</Label>
|
||||
{compound && (
|
||||
<Badge variant="secondary" className="text-[10px]">
|
||||
Complex condition
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{!compound && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs"
|
||||
onClick={() => {
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText) as Record<string, unknown>
|
||||
onChange(parsed)
|
||||
setJsonMode(false)
|
||||
} catch {
|
||||
// stay in JSON mode
|
||||
}
|
||||
}}
|
||||
>
|
||||
Switch to form
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Textarea
|
||||
className="font-mono text-xs min-h-24"
|
||||
value={jsonText}
|
||||
onChange={(e) => {
|
||||
setJsonText(e.target.value)
|
||||
try {
|
||||
const parsed = JSON.parse(e.target.value) as Record<string, unknown>
|
||||
onChange(parsed)
|
||||
} catch {
|
||||
// don't update on invalid JSON
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const predicate: SimplePredicate = simple
|
||||
? { field: value.field as string, operator: value.operator as string, value: String(value.value) }
|
||||
: { field: 'competitionCategory', operator: 'equals', value: '' }
|
||||
|
||||
const updateField = (field: string, val: string) => {
|
||||
const next = { ...predicate, [field]: val }
|
||||
onChange(next as unknown as Record<string, unknown>)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<Label className="text-xs">Condition</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 text-xs gap-1"
|
||||
onClick={() => {
|
||||
setJsonText(JSON.stringify(value, null, 2))
|
||||
setJsonMode(true)
|
||||
}}
|
||||
>
|
||||
<Code className="h-3 w-3" />
|
||||
Edit as JSON
|
||||
</Button>
|
||||
</div>
|
||||
<div className="grid gap-2 sm:grid-cols-3">
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-muted-foreground">Field</Label>
|
||||
<Select
|
||||
value={predicate.field}
|
||||
onValueChange={(v) => updateField('field', v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{FIELD_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-muted-foreground">Operator</Label>
|
||||
<Select
|
||||
value={predicate.operator}
|
||||
onValueChange={(v) => updateField('operator', v)}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{OPERATOR_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label className="text-[10px] text-muted-foreground">Value</Label>
|
||||
<Input
|
||||
className="h-8 text-xs"
|
||||
value={predicate.value}
|
||||
onChange={(e) => updateField('value', e.target.value)}
|
||||
placeholder="e.g. STARTUP"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@ import { Button } from '@/components/ui/button'
|
|||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { PredicateBuilder } from '@/components/admin/pipeline/predicate-builder'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
|
@ -52,7 +52,7 @@ type RuleDraft = {
|
|||
destinationStageId: string | null
|
||||
priority: number
|
||||
isActive: boolean
|
||||
predicateText: string
|
||||
predicateJson: Record<string, unknown>
|
||||
}
|
||||
|
||||
const DEFAULT_PREDICATE = {
|
||||
|
|
@ -119,7 +119,7 @@ export function RoutingRulesEditor({
|
|||
destinationStageId: rule.destinationStageId ?? null,
|
||||
priority: rule.priority,
|
||||
isActive: rule.isActive,
|
||||
predicateText: JSON.stringify(rule.predicateJson ?? {}, null, 2),
|
||||
predicateJson: (rule.predicateJson as Record<string, unknown>) ?? {},
|
||||
}
|
||||
}
|
||||
setDrafts(nextDrafts)
|
||||
|
|
@ -148,14 +148,6 @@ export function RoutingRulesEditor({
|
|||
const draft = drafts[id]
|
||||
if (!draft) return
|
||||
|
||||
let predicateJson: Record<string, unknown>
|
||||
try {
|
||||
predicateJson = JSON.parse(draft.predicateText) as Record<string, unknown>
|
||||
} catch {
|
||||
toast.error('Predicate must be valid JSON')
|
||||
return
|
||||
}
|
||||
|
||||
await upsertRule.mutateAsync({
|
||||
id: draft.id,
|
||||
pipelineId,
|
||||
|
|
@ -166,7 +158,7 @@ export function RoutingRulesEditor({
|
|||
destinationStageId: draft.destinationStageId,
|
||||
priority: draft.priority,
|
||||
isActive: draft.isActive,
|
||||
predicateJson,
|
||||
predicateJson: draft.predicateJson,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -370,19 +362,15 @@ export function RoutingRulesEditor({
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label className="text-xs">Predicate (JSON)</Label>
|
||||
<Textarea
|
||||
className="font-mono text-xs min-h-24"
|
||||
value={draft.predicateText}
|
||||
onChange={(e) =>
|
||||
setDrafts((prev) => ({
|
||||
...prev,
|
||||
[rule.id]: { ...draft, predicateText: e.target.value },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<PredicateBuilder
|
||||
value={draft.predicateJson}
|
||||
onChange={(predicate) =>
|
||||
setDrafts((prev) => ({
|
||||
...prev,
|
||||
[rule.id]: { ...draft, predicateJson: predicate },
|
||||
}))
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { EditableCard } from '@/components/ui/editable-card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
'use client'
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react'
|
||||
import { EditableCard } from '@/components/ui/editable-card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
import {
|
||||
Inbox,
|
||||
|
|
@ -13,30 +15,30 @@ import {
|
|||
BarChart3,
|
||||
} from 'lucide-react'
|
||||
|
||||
import { IntakeSection } from '@/components/admin/pipeline/sections/intake-section'
|
||||
import { FilteringSection } from '@/components/admin/pipeline/sections/filtering-section'
|
||||
import { AssignmentSection } from '@/components/admin/pipeline/sections/assignment-section'
|
||||
import { LiveFinalsSection } from '@/components/admin/pipeline/sections/live-finals-section'
|
||||
import { SelectionSection } from '@/components/admin/pipeline/sections/selection-section'
|
||||
import { ResultsSection } from '@/components/admin/pipeline/sections/results-section'
|
||||
|
||||
import {
|
||||
defaultIntakeConfig,
|
||||
defaultFilterConfig,
|
||||
defaultEvaluationConfig,
|
||||
defaultLiveConfig,
|
||||
defaultSelectionConfig,
|
||||
defaultResultsConfig,
|
||||
} from '@/lib/pipeline-defaults'
|
||||
|
||||
import type {
|
||||
IntakeConfig,
|
||||
FilterConfig,
|
||||
EvaluationConfig,
|
||||
LiveFinalConfig,
|
||||
SelectionConfig,
|
||||
ResultsConfig,
|
||||
} from '@/types/pipeline-wizard'
|
||||
import { IntakeSection } from '@/components/admin/pipeline/sections/intake-section'
|
||||
import { FilteringSection } from '@/components/admin/pipeline/sections/filtering-section'
|
||||
import { AssignmentSection } from '@/components/admin/pipeline/sections/assignment-section'
|
||||
import { LiveFinalsSection } from '@/components/admin/pipeline/sections/live-finals-section'
|
||||
import { SelectionSection } from '@/components/admin/pipeline/sections/selection-section'
|
||||
import { ResultsSection } from '@/components/admin/pipeline/sections/results-section'
|
||||
|
||||
import {
|
||||
defaultIntakeConfig,
|
||||
defaultFilterConfig,
|
||||
defaultEvaluationConfig,
|
||||
defaultLiveConfig,
|
||||
defaultSelectionConfig,
|
||||
defaultResultsConfig,
|
||||
} from '@/lib/pipeline-defaults'
|
||||
|
||||
import type {
|
||||
IntakeConfig,
|
||||
FilterConfig,
|
||||
EvaluationConfig,
|
||||
LiveFinalConfig,
|
||||
SelectionConfig,
|
||||
ResultsConfig,
|
||||
} from '@/types/pipeline-wizard'
|
||||
|
||||
type StageConfigEditorProps = {
|
||||
stageId: string
|
||||
|
|
@ -45,6 +47,7 @@ type StageConfigEditorProps = {
|
|||
configJson: Record<string, unknown> | null
|
||||
onSave: (stageId: string, configJson: Record<string, unknown>) => Promise<void>
|
||||
isSaving?: boolean
|
||||
alwaysEditable?: boolean
|
||||
}
|
||||
|
||||
const stageIcons: Record<string, React.ReactNode> = {
|
||||
|
|
@ -248,21 +251,22 @@ function ConfigSummary({
|
|||
}
|
||||
}
|
||||
|
||||
export function StageConfigEditor({
|
||||
export function StageConfigEditor({
|
||||
stageId,
|
||||
stageName,
|
||||
stageType,
|
||||
configJson,
|
||||
onSave,
|
||||
isSaving = false,
|
||||
alwaysEditable = false,
|
||||
}: StageConfigEditorProps) {
|
||||
const [localConfig, setLocalConfig] = useState<Record<string, unknown>>(
|
||||
() => configJson ?? {}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalConfig(configJson ?? {})
|
||||
}, [stageId, configJson])
|
||||
const [localConfig, setLocalConfig] = useState<Record<string, unknown>>(
|
||||
() => configJson ?? {}
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setLocalConfig(configJson ?? {})
|
||||
}, [stageId, configJson])
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
await onSave(stageId, localConfig)
|
||||
|
|
@ -350,29 +354,52 @@ export function StageConfigEditor({
|
|||
/>
|
||||
)
|
||||
}
|
||||
case 'SELECTION':
|
||||
return (
|
||||
<SelectionSection
|
||||
config={{
|
||||
...defaultSelectionConfig(),
|
||||
...(localConfig as SelectionConfig),
|
||||
}}
|
||||
onChange={(c) => setLocalConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
)
|
||||
case 'RESULTS':
|
||||
return (
|
||||
<ResultsSection
|
||||
config={{
|
||||
...defaultResultsConfig(),
|
||||
...(localConfig as ResultsConfig),
|
||||
}}
|
||||
onChange={(c) => setLocalConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
case 'SELECTION':
|
||||
return (
|
||||
<SelectionSection
|
||||
config={{
|
||||
...defaultSelectionConfig(),
|
||||
...(localConfig as SelectionConfig),
|
||||
}}
|
||||
onChange={(c) => setLocalConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
)
|
||||
case 'RESULTS':
|
||||
return (
|
||||
<ResultsSection
|
||||
config={{
|
||||
...defaultResultsConfig(),
|
||||
...(localConfig as ResultsConfig),
|
||||
}}
|
||||
onChange={(c) => setLocalConfig(c as unknown as Record<string, unknown>)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
if (alwaysEditable) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
{stageIcons[stageType] && (
|
||||
<span className="text-muted-foreground">{stageIcons[stageType]}</span>
|
||||
)}
|
||||
<h3 className="text-sm font-semibold">{stageName} Configuration</h3>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{stageType.replace('_', ' ')}
|
||||
</Badge>
|
||||
</div>
|
||||
{renderEditor()}
|
||||
<div className="flex justify-end pt-2 border-t">
|
||||
<Button size="sm" onClick={handleSave} disabled={isSaving}>
|
||||
{isSaving && <Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -0,0 +1,178 @@
|
|||
'use client'
|
||||
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
} from '@/components/ui/sheet'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
import { StageConfigEditor } from '@/components/admin/pipeline/stage-config-editor'
|
||||
import { FileRequirementsEditor } from '@/components/admin/file-requirements-editor'
|
||||
import { FilteringRulesEditor } from '@/components/admin/pipeline/filtering-rules-editor'
|
||||
|
||||
import { IntakePanel } from '@/components/admin/pipeline/stage-panels/intake-panel'
|
||||
import { FilterPanel } from '@/components/admin/pipeline/stage-panels/filter-panel'
|
||||
import { EvaluationPanel } from '@/components/admin/pipeline/stage-panels/evaluation-panel'
|
||||
import { SelectionPanel } from '@/components/admin/pipeline/stage-panels/selection-panel'
|
||||
import { LiveFinalPanel } from '@/components/admin/pipeline/stage-panels/live-final-panel'
|
||||
import { ResultsPanel } from '@/components/admin/pipeline/stage-panels/results-panel'
|
||||
|
||||
type StageType = 'INTAKE' | 'FILTER' | 'EVALUATION' | 'SELECTION' | 'LIVE_FINAL' | 'RESULTS'
|
||||
|
||||
type StageDetailSheetProps = {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
stage: {
|
||||
id: string
|
||||
name: string
|
||||
stageType: StageType
|
||||
configJson: Record<string, unknown> | null
|
||||
} | null
|
||||
onSaveConfig: (stageId: string, configJson: Record<string, unknown>) => Promise<void>
|
||||
isSaving: boolean
|
||||
pipelineId: string
|
||||
materializeRequirements?: (stageId: string) => void
|
||||
isMaterializing?: boolean
|
||||
}
|
||||
|
||||
function StagePanelContent({
|
||||
stageId,
|
||||
stageType,
|
||||
configJson,
|
||||
}: {
|
||||
stageId: string
|
||||
stageType: string
|
||||
configJson: Record<string, unknown> | null
|
||||
}) {
|
||||
switch (stageType) {
|
||||
case 'INTAKE':
|
||||
return <IntakePanel stageId={stageId} configJson={configJson} />
|
||||
case 'FILTER':
|
||||
return <FilterPanel stageId={stageId} configJson={configJson} />
|
||||
case 'EVALUATION':
|
||||
return <EvaluationPanel stageId={stageId} configJson={configJson} />
|
||||
case 'SELECTION':
|
||||
return <SelectionPanel stageId={stageId} configJson={configJson} />
|
||||
case 'LIVE_FINAL':
|
||||
return <LiveFinalPanel stageId={stageId} configJson={configJson} />
|
||||
case 'RESULTS':
|
||||
return <ResultsPanel stageId={stageId} configJson={configJson} />
|
||||
default:
|
||||
return (
|
||||
<p className="text-sm text-muted-foreground py-4">
|
||||
Unknown stage type: {stageType}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const stageTypeLabels: Record<string, string> = {
|
||||
INTAKE: 'Intake',
|
||||
FILTER: 'Filter',
|
||||
EVALUATION: 'Evaluation',
|
||||
SELECTION: 'Selection',
|
||||
LIVE_FINAL: 'Live Final',
|
||||
RESULTS: 'Results',
|
||||
}
|
||||
|
||||
export function StageDetailSheet({
|
||||
open,
|
||||
onOpenChange,
|
||||
stage,
|
||||
onSaveConfig,
|
||||
isSaving,
|
||||
pipelineId: _pipelineId,
|
||||
materializeRequirements,
|
||||
isMaterializing = false,
|
||||
}: StageDetailSheetProps) {
|
||||
if (!stage) return null
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-full sm:w-[540px] lg:w-[640px] sm:max-w-[640px] overflow-y-auto p-0"
|
||||
>
|
||||
<div className="p-6 pb-0">
|
||||
<SheetHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<SheetTitle className="text-base">{stage.name}</SheetTitle>
|
||||
<Badge variant="outline" className="text-[10px]">
|
||||
{stageTypeLabels[stage.stageType] ?? stage.stageType}
|
||||
</Badge>
|
||||
</div>
|
||||
<SheetDescription>
|
||||
Configure settings and view activity for this stage
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
</div>
|
||||
|
||||
<div className="px-6 pt-4 pb-6">
|
||||
<Tabs defaultValue="configuration">
|
||||
<TabsList className="w-full">
|
||||
<TabsTrigger value="configuration" className="flex-1">
|
||||
Configuration
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity" className="flex-1">
|
||||
Activity
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="configuration" className="space-y-4 mt-4">
|
||||
<StageConfigEditor
|
||||
stageId={stage.id}
|
||||
stageName={stage.name}
|
||||
stageType={stage.stageType}
|
||||
configJson={stage.configJson}
|
||||
onSave={onSaveConfig}
|
||||
isSaving={isSaving}
|
||||
alwaysEditable
|
||||
/>
|
||||
|
||||
{stage.stageType === 'INTAKE' && (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h3 className="text-sm font-medium">Intake File Requirements</h3>
|
||||
{materializeRequirements && (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => materializeRequirements(stage.id)}
|
||||
disabled={isMaterializing}
|
||||
>
|
||||
{isMaterializing && (
|
||||
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" />
|
||||
)}
|
||||
Import Legacy Requirements
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<FileRequirementsEditor stageId={stage.id} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{stage.stageType === 'FILTER' && (
|
||||
<FilteringRulesEditor stageId={stage.id} />
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="activity" className="mt-4">
|
||||
<StagePanelContent
|
||||
stageId={stage.id}
|
||||
stageType={stage.stageType}
|
||||
configJson={stage.configJson}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import * as SheetPrimitive from '@radix-ui/react-dialog'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
import { X } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const Sheet = SheetPrimitive.Root
|
||||
const SheetTrigger = SheetPrimitive.Trigger
|
||||
const SheetClose = SheetPrimitive.Close
|
||||
const SheetPortal = SheetPrimitive.Portal
|
||||
|
||||
const SheetOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
'fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}
|
||||
/>
|
||||
))
|
||||
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
|
||||
|
||||
const sheetVariants = cva(
|
||||
'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500 data-[state=open]:animate-in data-[state=closed]:animate-out',
|
||||
{
|
||||
variants: {
|
||||
side: {
|
||||
top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
|
||||
bottom:
|
||||
'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
|
||||
left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
|
||||
right:
|
||||
'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
side: 'right',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
type SheetContentProps = React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content> &
|
||||
VariantProps<typeof sheetVariants>
|
||||
|
||||
const SheetContent = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Content>,
|
||||
SheetContentProps
|
||||
>(({ side = 'right', className, children, ...props }, ref) => (
|
||||
<SheetPortal>
|
||||
<SheetOverlay />
|
||||
<SheetPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(sheetVariants({ side }), className)}
|
||||
{...props}
|
||||
>
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
{children}
|
||||
</SheetPrimitive.Content>
|
||||
</SheetPortal>
|
||||
))
|
||||
SheetContent.displayName = SheetPrimitive.Content.displayName
|
||||
|
||||
const SheetHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col space-y-2 text-center sm:text-left',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetHeader.displayName = 'SheetHeader'
|
||||
|
||||
const SheetFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
'flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
SheetFooter.displayName = 'SheetFooter'
|
||||
|
||||
const SheetTitle = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn('text-lg font-semibold text-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetTitle.displayName = SheetPrimitive.Title.displayName
|
||||
|
||||
const SheetDescription = React.forwardRef<
|
||||
React.ElementRef<typeof SheetPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn('text-sm text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
SheetDescription.displayName = SheetPrimitive.Description.displayName
|
||||
|
||||
export {
|
||||
Sheet,
|
||||
SheetPortal,
|
||||
SheetOverlay,
|
||||
SheetTrigger,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetFooter,
|
||||
SheetTitle,
|
||||
SheetDescription,
|
||||
}
|
||||
Loading…
Reference in New Issue