feat: GitHub-link op DONE-card + pushed_at doorvoer
- lib/job-status-url.ts: getBranchUrl(repoUrl, branch) → GitHub tree URL - JobState + ClaudeJobEvent: pushed_at? veld toegevoegd - realtime/solo/route.ts: pushed_at in Prisma-select, JobPayload en mapping - SoloBoardProps + TaskDetailDialog: repoUrl prop doorgevoerd - task-detail-dialog: "Open op GitHub"-link als done + pushed_at + branch + repoUrl - 3 unit-tests voor getBranchUrl; totaal 261 tests groen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
120a05347b
commit
f59f4754df
7 changed files with 55 additions and 9 deletions
22
__tests__/lib/job-status-url.test.ts
Normal file
22
__tests__/lib/job-status-url.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { getBranchUrl } from '@/lib/job-status-url'
|
||||
|
||||
describe('getBranchUrl', () => {
|
||||
it('builds a GitHub tree URL from repo URL and branch', () => {
|
||||
expect(getBranchUrl('https://github.com/owner/repo', 'feat/job-abc12345')).toBe(
|
||||
'https://github.com/owner/repo/tree/feat/job-abc12345',
|
||||
)
|
||||
})
|
||||
|
||||
it('strips trailing .git suffix', () => {
|
||||
expect(getBranchUrl('https://github.com/owner/repo.git', 'feat/job-abc')).toBe(
|
||||
'https://github.com/owner/repo/tree/feat/job-abc',
|
||||
)
|
||||
})
|
||||
|
||||
it('strips trailing slash', () => {
|
||||
expect(getBranchUrl('https://github.com/owner/repo/', 'feat/job-abc')).toBe(
|
||||
'https://github.com/owner/repo/tree/feat/job-abc',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
|
@ -110,6 +110,7 @@ export default async function SoloProductPage({ params }: Props) {
|
|||
unassignedStories={unassignedStories}
|
||||
isDemo={session.isDemo ?? false}
|
||||
currentUserId={session.userId}
|
||||
repoUrl={product.repo_url}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ type JobPayload = {
|
|||
product_id: string
|
||||
status: string
|
||||
branch?: string
|
||||
pushed_at?: string
|
||||
summary?: string
|
||||
error?: string
|
||||
}
|
||||
|
|
@ -258,7 +259,7 @@ async function prisma_jobs_findActive(userId: string, productId: string) {
|
|||
],
|
||||
},
|
||||
select: {
|
||||
id: true, task_id: true, status: true, branch: true, summary: true, error: true,
|
||||
id: true, task_id: true, status: true, branch: true, pushed_at: true, summary: true, error: true,
|
||||
},
|
||||
orderBy: { created_at: 'asc' },
|
||||
})
|
||||
|
|
@ -267,6 +268,7 @@ async function prisma_jobs_findActive(userId: string, productId: string) {
|
|||
task_id: j.task_id,
|
||||
status: jobStatusToApi(j.status),
|
||||
branch: j.branch ?? undefined,
|
||||
pushed_at: j.pushed_at?.toISOString() ?? undefined,
|
||||
summary: j.summary ?? undefined,
|
||||
error: j.error ?? undefined,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export interface SoloBoardProps {
|
|||
unassignedStories: UnassignedStory[]
|
||||
isDemo: boolean
|
||||
currentUserId: string
|
||||
repoUrl?: string | null
|
||||
}
|
||||
|
||||
const COLUMN_STATUSES: ColumnStatus[] = ['TO_DO', 'IN_PROGRESS', 'DONE']
|
||||
|
|
@ -48,7 +49,7 @@ function getColumnStatus(status: SoloTask['status']): ColumnStatus {
|
|||
}
|
||||
|
||||
export function SoloBoard({
|
||||
productId, sprintGoal, tasks: initialTasks, unassignedStories: initialUnassigned, isDemo,
|
||||
productId, sprintGoal, tasks: initialTasks, unassignedStories: initialUnassigned, isDemo, repoUrl,
|
||||
}: SoloBoardProps) {
|
||||
const { tasks, initTasks, optimisticMove, rollback, markPending, clearPending } = useSoloStore()
|
||||
const claudeJobsByTaskId = useSoloStore((s) => s.claudeJobsByTaskId)
|
||||
|
|
@ -219,6 +220,7 @@ export function SoloBoard({
|
|||
task={selectedTask}
|
||||
productId={productId}
|
||||
isDemo={isDemo}
|
||||
repoUrl={repoUrl}
|
||||
onClose={() => setSelectedTask(null)}
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/comp
|
|||
import { useSoloStore } from '@/stores/solo-store'
|
||||
import { enqueueClaudeJobAction, cancelClaudeJobAction } from '@/actions/claude-jobs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { getBranchUrl } from '@/lib/job-status-url'
|
||||
import type { SoloTask } from './solo-board'
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
|
|
@ -33,6 +34,7 @@ interface TaskDetailDialogProps {
|
|||
task: SoloTask | null
|
||||
productId: string
|
||||
isDemo: boolean
|
||||
repoUrl?: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
|
|
@ -40,12 +42,13 @@ interface TaskDetailContentProps {
|
|||
task: SoloTask
|
||||
productId: string
|
||||
isDemo: boolean
|
||||
repoUrl?: string | null
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
type SaveState = 'idle' | 'saving' | 'saved'
|
||||
|
||||
function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailContentProps) {
|
||||
function TaskDetailContent({ task, productId, isDemo, repoUrl, onClose }: TaskDetailContentProps) {
|
||||
const { updatePlan } = useSoloStore()
|
||||
const job = useSoloStore(s => s.claudeJobsByTaskId[task.id])
|
||||
const connectedWorkers = useSoloStore(s => s.connectedWorkers)
|
||||
|
|
@ -206,8 +209,18 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
)}
|
||||
|
||||
{job?.status === 'done' && (
|
||||
<span className="text-xs text-status-done">
|
||||
Klaar{job.branch ? ` — branch ${job.branch}` : ''}
|
||||
<span className="text-xs text-status-done flex items-center gap-2">
|
||||
Klaar{job.branch && !job.pushed_at ? ` — branch ${job.branch}` : ''}
|
||||
{job.pushed_at && job.branch && repoUrl && (
|
||||
<a
|
||||
href={getBranchUrl(repoUrl, job.branch)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="underline text-primary hover:text-primary/80"
|
||||
>
|
||||
Open op GitHub
|
||||
</a>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
|
|
@ -219,7 +232,7 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte
|
|||
)
|
||||
}
|
||||
|
||||
export function TaskDetailDialog({ task, productId, isDemo, onClose }: TaskDetailDialogProps) {
|
||||
export function TaskDetailDialog({ task, productId, isDemo, repoUrl, onClose }: TaskDetailDialogProps) {
|
||||
return (
|
||||
<Dialog open={!!task} onOpenChange={(open) => { if (!open) onClose() }}>
|
||||
<DialogContent className="sm:max-w-lg">
|
||||
|
|
@ -229,6 +242,7 @@ export function TaskDetailDialog({ task, productId, isDemo, onClose }: TaskDetai
|
|||
task={task}
|
||||
productId={productId}
|
||||
isDemo={isDemo}
|
||||
repoUrl={repoUrl}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
4
lib/job-status-url.ts
Normal file
4
lib/job-status-url.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
export function getBranchUrl(repoUrl: string, branch: string): string {
|
||||
const base = repoUrl.replace(/\.git$/, '').replace(/\/$/, '')
|
||||
return `${base}/tree/${branch}`
|
||||
}
|
||||
|
|
@ -9,13 +9,14 @@ export interface JobState {
|
|||
task_id: string
|
||||
status: ClaudeJobStatusApi
|
||||
branch?: string
|
||||
pushed_at?: string | null
|
||||
summary?: string
|
||||
error?: string
|
||||
}
|
||||
|
||||
export type ClaudeJobEvent =
|
||||
| { type: 'claude_job_enqueued'; job_id: string; task_id: string; user_id: string; product_id: string; status: 'queued' }
|
||||
| { type: 'claude_job_status'; job_id: string; task_id: string; user_id: string; product_id: string; status: ClaudeJobStatusApi; branch?: string; summary?: string; error?: string }
|
||||
| { type: 'claude_job_status'; job_id: string; task_id: string; user_id: string; product_id: string; status: ClaudeJobStatusApi; branch?: string; pushed_at?: string; summary?: string; error?: string }
|
||||
|
||||
// Payload-shape gepubliceerd door de Postgres-trigger via pg_notify (ST-801
|
||||
// + ST-804 prereq). Komt het Solo Paneel binnen via de SSE-stream uit
|
||||
|
|
@ -146,7 +147,7 @@ export const useSoloStore = create<SoloStore>((set, get) => ({
|
|||
return
|
||||
}
|
||||
if (event.type === 'claude_job_status') {
|
||||
const { status, branch, summary, error } = event
|
||||
const { status, branch, pushed_at, summary, error } = event
|
||||
if (status === 'cancelled') {
|
||||
set((s) => {
|
||||
const next = { ...s.claudeJobsByTaskId }
|
||||
|
|
@ -158,7 +159,7 @@ export const useSoloStore = create<SoloStore>((set, get) => ({
|
|||
set((s) => ({
|
||||
claudeJobsByTaskId: {
|
||||
...s.claudeJobsByTaskId,
|
||||
[task_id]: { job_id, task_id, status, branch, summary, error },
|
||||
[task_id]: { job_id, task_id, status, branch, pushed_at, summary, error },
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue