feat(ui): add verify_required select to TaskDetailDialog
SoloTask interface, solo page mapping, solo store, PATCH route handler and TaskDetailDialog all updated to expose the three-level verify gate (ALIGNED / ALIGNED_OR_PARTIAL / ANY) as a native select. Disabled with DemoTooltip in demo mode. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8ac935daa9
commit
c7bd42c4e4
7 changed files with 72 additions and 6 deletions
|
|
@ -60,6 +60,7 @@ const baseTask: SoloTask = {
|
||||||
sort_order: 1,
|
sort_order: 1,
|
||||||
status: 'TO_DO',
|
status: 'TO_DO',
|
||||||
verify_only: false,
|
verify_only: false,
|
||||||
|
verify_required: 'ALIGNED_OR_PARTIAL',
|
||||||
story_id: 'story-1',
|
story_id: 'story-1',
|
||||||
story_code: 'ST-100',
|
story_code: 'ST-100',
|
||||||
story_title: 'Test Story',
|
story_title: 'Test Story',
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ const baseTask = (id: string, overrides: Partial<SoloTask> = {}): SoloTask => ({
|
||||||
sort_order: 1,
|
sort_order: 1,
|
||||||
status: 'TO_DO',
|
status: 'TO_DO',
|
||||||
verify_only: false,
|
verify_only: false,
|
||||||
|
verify_required: 'ALIGNED_OR_PARTIAL',
|
||||||
story_id: 'story-1',
|
story_id: 'story-1',
|
||||||
story_code: 'ST-100',
|
story_code: 'ST-100',
|
||||||
story_title: 'Original Story',
|
story_title: 'Original Story',
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,7 @@ export default async function SoloProductPage({ params }: Props) {
|
||||||
sort_order: t.sort_order,
|
sort_order: t.sort_order,
|
||||||
status: t.status as SoloTask['status'],
|
status: t.status as SoloTask['status'],
|
||||||
verify_only: t.verify_only,
|
verify_only: t.verify_only,
|
||||||
|
verify_required: t.verify_required as SoloTask['verify_required'],
|
||||||
story_id: t.story.id,
|
story_id: t.story.id,
|
||||||
story_code: t.story.code,
|
story_code: t.story.code,
|
||||||
story_title: t.story.title,
|
story_title: t.story.title,
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,22 @@ import { updateTaskStatusWithStoryPromotion } from '@/lib/tasks-status-update'
|
||||||
// drive tasks into a state the shared UI can't display.
|
// drive tasks into a state the shared UI can't display.
|
||||||
const PATCHABLE_TASK_STATUS = TASK_STATUS_API_VALUES.filter((s) => s !== 'review')
|
const PATCHABLE_TASK_STATUS = TASK_STATUS_API_VALUES.filter((s) => s !== 'review')
|
||||||
|
|
||||||
|
const VERIFY_REQUIRED_VALUES = ['ALIGNED', 'ALIGNED_OR_PARTIAL', 'ANY'] as const
|
||||||
|
|
||||||
const patchSchema = z
|
const patchSchema = z
|
||||||
.object({
|
.object({
|
||||||
status: z.enum(PATCHABLE_TASK_STATUS as [string, ...string[]]).optional(),
|
status: z.enum(PATCHABLE_TASK_STATUS as [string, ...string[]]).optional(),
|
||||||
implementation_plan: z.string().optional(),
|
implementation_plan: z.string().optional(),
|
||||||
verify_only: z.boolean().optional(),
|
verify_only: z.boolean().optional(),
|
||||||
|
verify_required: z.enum(VERIFY_REQUIRED_VALUES).optional(),
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) =>
|
(data) =>
|
||||||
data.status !== undefined ||
|
data.status !== undefined ||
|
||||||
data.implementation_plan !== undefined ||
|
data.implementation_plan !== undefined ||
|
||||||
data.verify_only !== undefined,
|
data.verify_only !== undefined ||
|
||||||
{ message: 'Geef minimaal status, implementation_plan of verify_only mee' },
|
data.verify_required !== undefined,
|
||||||
|
{ message: 'Geef minimaal status, implementation_plan, verify_only of verify_required mee' },
|
||||||
)
|
)
|
||||||
|
|
||||||
export async function PATCH(
|
export async function PATCH(
|
||||||
|
|
@ -88,19 +92,21 @@ export async function PATCH(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Combine simple field writes (plan, verify_only) into one update call
|
// Combine simple field writes (plan, verify_only, verify_required) into one update call
|
||||||
const simpleData: { implementation_plan?: string; verify_only?: boolean } = {}
|
const simpleData: { implementation_plan?: string; verify_only?: boolean; verify_required?: typeof VERIFY_REQUIRED_VALUES[number] } = {}
|
||||||
if (parsed.data.implementation_plan !== undefined)
|
if (parsed.data.implementation_plan !== undefined)
|
||||||
simpleData.implementation_plan = parsed.data.implementation_plan
|
simpleData.implementation_plan = parsed.data.implementation_plan
|
||||||
if (parsed.data.verify_only !== undefined)
|
if (parsed.data.verify_only !== undefined)
|
||||||
simpleData.verify_only = parsed.data.verify_only
|
simpleData.verify_only = parsed.data.verify_only
|
||||||
|
if (parsed.data.verify_required !== undefined)
|
||||||
|
simpleData.verify_required = parsed.data.verify_required
|
||||||
|
|
||||||
const updated = await prisma.$transaction(async (tx) => {
|
const updated = await prisma.$transaction(async (tx) => {
|
||||||
const simpleUpdate = Object.keys(simpleData).length > 0
|
const simpleUpdate = Object.keys(simpleData).length > 0
|
||||||
? await tx.task.update({
|
? await tx.task.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: simpleData,
|
data: simpleData,
|
||||||
select: { id: true, status: true, implementation_plan: true, verify_only: true },
|
select: { id: true, status: true, implementation_plan: true, verify_only: true, verify_required: true },
|
||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
|
|
@ -111,6 +117,7 @@ export async function PATCH(
|
||||||
status: result.task.status,
|
status: result.task.status,
|
||||||
implementation_plan: result.task.implementation_plan,
|
implementation_plan: result.task.implementation_plan,
|
||||||
verify_only: simpleUpdate?.verify_only,
|
verify_only: simpleUpdate?.verify_only,
|
||||||
|
verify_required: simpleUpdate?.verify_required,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -125,5 +132,6 @@ export async function PATCH(
|
||||||
status: taskStatusToApi(updated.status),
|
status: taskStatusToApi(updated.status),
|
||||||
implementation_plan: updated.implementation_plan,
|
implementation_plan: updated.implementation_plan,
|
||||||
...(updated.verify_only !== undefined && { verify_only: updated.verify_only }),
|
...(updated.verify_only !== undefined && { verify_only: updated.verify_only }),
|
||||||
|
...(updated.verify_required !== undefined && { verify_required: updated.verify_required }),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ export interface SoloTask {
|
||||||
sort_order: number
|
sort_order: number
|
||||||
status: 'TO_DO' | 'IN_PROGRESS' | 'REVIEW' | 'DONE'
|
status: 'TO_DO' | 'IN_PROGRESS' | 'REVIEW' | 'DONE'
|
||||||
verify_only: boolean
|
verify_only: boolean
|
||||||
|
verify_required: 'ALIGNED' | 'ALIGNED_OR_PARTIAL' | 'ANY'
|
||||||
story_id: string
|
story_id: string
|
||||||
story_code: string | null
|
story_code: string | null
|
||||||
story_title: string
|
story_title: string
|
||||||
|
|
|
||||||
|
|
@ -55,16 +55,24 @@ const VERIFY_RESULT_CONFIG: Record<string, { label: string; className: string }>
|
||||||
|
|
||||||
type SaveState = 'idle' | 'saving' | 'saved'
|
type SaveState = 'idle' | 'saving' | 'saved'
|
||||||
|
|
||||||
|
const VERIFY_REQUIRED_LABELS: Record<string, string> = {
|
||||||
|
ALIGNED: 'Strikt — alleen ALIGNED',
|
||||||
|
ALIGNED_OR_PARTIAL: 'Standaard — ALIGNED of PARTIAL met uitleg',
|
||||||
|
ANY: 'Vrij — geen verify-eis',
|
||||||
|
}
|
||||||
|
|
||||||
function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDetailContentProps) {
|
function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDetailContentProps) {
|
||||||
const { updatePlan, updateVerifyOnly } = useSoloStore()
|
const { updatePlan, updateVerifyOnly, updateVerifyRequired } = useSoloStore()
|
||||||
const job = useSoloStore(s => s.claudeJobsByTaskId[task.id])
|
const job = useSoloStore(s => s.claudeJobsByTaskId[task.id])
|
||||||
const connectedWorkers = useSoloStore(s => s.connectedWorkers)
|
const connectedWorkers = useSoloStore(s => s.connectedWorkers)
|
||||||
const [localPlan, setLocalPlan] = useState(task.implementation_plan ?? '')
|
const [localPlan, setLocalPlan] = useState(task.implementation_plan ?? '')
|
||||||
const [localVerifyOnly, setLocalVerifyOnly] = useState(task.verify_only)
|
const [localVerifyOnly, setLocalVerifyOnly] = useState(task.verify_only)
|
||||||
|
const [localVerifyRequired, setLocalVerifyRequired] = useState(task.verify_required)
|
||||||
const [saveState, setSaveState] = useState<SaveState>('idle')
|
const [saveState, setSaveState] = useState<SaveState>('idle')
|
||||||
const [, startTransition] = useTransition()
|
const [, startTransition] = useTransition()
|
||||||
const [jobPending, startJobTransition] = useTransition()
|
const [jobPending, startJobTransition] = useTransition()
|
||||||
const [verifyOnlyPending, startVerifyOnlyTransition] = useTransition()
|
const [verifyOnlyPending, startVerifyOnlyTransition] = useTransition()
|
||||||
|
const [verifyRequiredPending, startVerifyRequiredTransition] = useTransition()
|
||||||
const fadeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const fadeTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
const savedPlanRef = useRef(task.implementation_plan ?? '')
|
const savedPlanRef = useRef(task.implementation_plan ?? '')
|
||||||
|
|
||||||
|
|
@ -145,6 +153,32 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleVerifyRequiredChange(e: React.ChangeEvent<HTMLSelectElement>) {
|
||||||
|
if (isDemo) return
|
||||||
|
const newValue = e.target.value as typeof localVerifyRequired
|
||||||
|
const prevValue = localVerifyRequired
|
||||||
|
setLocalVerifyRequired(newValue)
|
||||||
|
startVerifyRequiredTransition(async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/tasks/${task.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
credentials: 'include',
|
||||||
|
body: JSON.stringify({ verify_required: newValue }),
|
||||||
|
})
|
||||||
|
if (!res.ok) {
|
||||||
|
setLocalVerifyRequired(prevValue)
|
||||||
|
toast.error('Verify-required bijwerken mislukt')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
updateVerifyRequired(task.id, newValue)
|
||||||
|
} catch {
|
||||||
|
setLocalVerifyRequired(prevValue)
|
||||||
|
toast.error('Verify-required bijwerken mislukt')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
|
|
@ -220,6 +254,22 @@ function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDe
|
||||||
<span className="text-xs text-muted-foreground">Alleen verifiëren (niet implementeren)</span>
|
<span className="text-xs text-muted-foreground">Alleen verifiëren (niet implementeren)</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<span className="text-xs text-muted-foreground shrink-0">Verify-gate:</span>
|
||||||
|
<DemoTooltip show={isDemo}>
|
||||||
|
<select
|
||||||
|
value={localVerifyRequired}
|
||||||
|
onChange={handleVerifyRequiredChange}
|
||||||
|
disabled={isDemo || verifyRequiredPending}
|
||||||
|
className="text-xs rounded-md border border-border bg-surface-container px-2 py-1 text-foreground focus:outline-none focus:ring-1 focus:ring-primary disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{(['ALIGNED', 'ALIGNED_OR_PARTIAL', 'ANY'] as const).map(v => (
|
||||||
|
<option key={v} value={v}>{VERIFY_REQUIRED_LABELS[v]}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</DemoTooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="-mx-4 -mb-4 flex flex-wrap items-center gap-2 border-t bg-muted/50 px-4 py-3 rounded-b-xl">
|
<div className="-mx-4 -mb-4 flex flex-wrap items-center gap-2 border-t bg-muted/50 px-4 py-3 rounded-b-xl">
|
||||||
<Link
|
<Link
|
||||||
href={`/products/${productId}/sprint/planning`}
|
href={`/products/${productId}/sprint/planning`}
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,7 @@ interface SoloStore {
|
||||||
rollback: (taskId: string, prevStatus: TaskStatus) => void
|
rollback: (taskId: string, prevStatus: TaskStatus) => void
|
||||||
updatePlan: (taskId: string, plan: string | null) => void
|
updatePlan: (taskId: string, plan: string | null) => void
|
||||||
updateVerifyOnly: (taskId: string, value: boolean) => void
|
updateVerifyOnly: (taskId: string, value: boolean) => void
|
||||||
|
updateVerifyRequired: (taskId: string, value: 'ALIGNED' | 'ALIGNED_OR_PARTIAL' | 'ANY') => void
|
||||||
|
|
||||||
markPending: (taskId: string) => void
|
markPending: (taskId: string) => void
|
||||||
clearPending: (taskId: string) => void
|
clearPending: (taskId: string) => void
|
||||||
|
|
@ -112,6 +113,9 @@ export const useSoloStore = create<SoloStore>((set, get) => ({
|
||||||
updateVerifyOnly: (taskId, value) =>
|
updateVerifyOnly: (taskId, value) =>
|
||||||
set((s) => ({ tasks: { ...s.tasks, [taskId]: { ...s.tasks[taskId], verify_only: value } } })),
|
set((s) => ({ tasks: { ...s.tasks, [taskId]: { ...s.tasks[taskId], verify_only: value } } })),
|
||||||
|
|
||||||
|
updateVerifyRequired: (taskId, value) =>
|
||||||
|
set((s) => ({ tasks: { ...s.tasks, [taskId]: { ...s.tasks[taskId], verify_required: value } } })),
|
||||||
|
|
||||||
markPending: (taskId) =>
|
markPending: (taskId) =>
|
||||||
set((s) => {
|
set((s) => {
|
||||||
if (s.pendingOps.has(taskId)) return s
|
if (s.pendingOps.has(taskId)) return s
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue