From b71eb53fa8270fd94d72adbe0160250ba77f30a3 Mon Sep 17 00:00:00 2001 From: Madhura68 Date: Sun, 26 Apr 2026 20:36:59 +0200 Subject: [PATCH] feat(ST-507): show code badges on cards, lists and dialogs across the app Co-Authored-By: Claude Opus 4.7 (1M context) --- app/(app)/products/[id]/page.tsx | 3 +- app/(app)/products/[id]/solo/page.tsx | 40 ++++++++++++++------ app/(app)/products/[id]/sprint/page.tsx | 3 ++ components/backlog/backlog-card.tsx | 9 ++++- components/backlog/pbi-list.tsx | 2 + components/backlog/story-panel.tsx | 2 + components/dashboard/product-list.tsx | 11 ++++-- components/solo/solo-board.tsx | 2 + components/solo/solo-task-card.tsx | 21 ++++++++-- components/solo/task-detail-dialog.tsx | 10 ++++- components/solo/unassigned-stories-sheet.tsx | 7 +++- components/sprint/sprint-backlog.tsx | 19 ++++++++-- components/sprint/sprint-board-client.tsx | 1 + components/sprint/sprint-header.tsx | 1 + components/sprint/task-list.tsx | 29 ++++++++------ 15 files changed, 122 insertions(+), 38 deletions(-) diff --git a/app/(app)/products/[id]/page.tsx b/app/(app)/products/[id]/page.tsx index b41c25e..92a0e87 100644 --- a/app/(app)/products/[id]/page.tsx +++ b/app/(app)/products/[id]/page.tsx @@ -35,6 +35,7 @@ export default async function ProductBacklogPage({ params }: Props) { orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }], select: { id: true, + code: true, title: true, description: true, acceptance_criteria: true, @@ -87,7 +88,7 @@ export default async function ProductBacklogPage({ params }: Props) { left={ ({ id: p.id, title: p.title, priority: p.priority, description: p.description }))} + pbis={pbis.map((p: (typeof pbis)[number]) => ({ id: p.id, code: p.code, title: p.title, priority: p.priority, description: p.description }))} isDemo={isDemo} /> } diff --git a/app/(app)/products/[id]/solo/page.tsx b/app/(app)/products/[id]/solo/page.tsx index 1e833c4..995aee2 100644 --- a/app/(app)/products/[id]/solo/page.tsx +++ b/app/(app)/products/[id]/solo/page.tsx @@ -40,7 +40,14 @@ export default async function SoloProductPage({ params }: Props) { }, }, include: { - story: { select: { id: true, title: true } }, + story: { + select: { + id: true, + code: true, + title: true, + tasks: { select: { id: true }, orderBy: { sort_order: 'asc' } }, + }, + }, }, orderBy: [ { story: { sort_order: 'asc' } }, @@ -52,6 +59,7 @@ export default async function SoloProductPage({ params }: Props) { where: { sprint_id: sprint.id, assignee_id: null }, select: { id: true, + code: true, title: true, tasks: { select: { id: true, title: true, description: true, priority: true, status: true }, @@ -62,20 +70,28 @@ export default async function SoloProductPage({ params }: Props) { }), ]) - const tasks: SoloTask[] = rawTasks.map(t => ({ - id: t.id, - title: t.title, - description: t.description, - implementation_plan: t.implementation_plan, - priority: t.priority, - sort_order: t.sort_order, - status: t.status as SoloTask['status'], - story_id: t.story.id, - story_title: t.story.title, - })) + const tasks: SoloTask[] = rawTasks.map(t => { + const positionInStory = t.story.tasks.findIndex(st => st.id === t.id) + const taskCode = + t.story.code && positionInStory >= 0 ? `${t.story.code}.${positionInStory + 1}` : null + return { + id: t.id, + title: t.title, + description: t.description, + implementation_plan: t.implementation_plan, + priority: t.priority, + sort_order: t.sort_order, + status: t.status as SoloTask['status'], + story_id: t.story.id, + story_code: t.story.code, + story_title: t.story.title, + task_code: taskCode, + } + }) const unassignedStories: UnassignedStory[] = rawUnassigned.map(s => ({ id: s.id, + code: s.code, title: s.title, tasks: s.tasks.map(t => ({ id: t.id, diff --git a/app/(app)/products/[id]/sprint/page.tsx b/app/(app)/products/[id]/sprint/page.tsx index 16885c5..0365842 100644 --- a/app/(app)/products/[id]/sprint/page.tsx +++ b/app/(app)/products/[id]/sprint/page.tsx @@ -49,6 +49,7 @@ export default async function SprintBoardPage({ params }: Props) { const sprintStoryItems: SprintStory[] = sprintStories.map(s => ({ id: s.id, + code: s.code, title: s.title, priority: s.priority, status: s.status, @@ -86,9 +87,11 @@ export default async function SprintBoardPage({ params }: Props) { .filter(pbi => pbi.stories.length > 0) .map(pbi => ({ id: pbi.id, + code: pbi.code, title: pbi.title, stories: pbi.stories.map(s => ({ id: s.id, + code: s.code, title: s.title, priority: s.priority, status: s.status, diff --git a/components/backlog/backlog-card.tsx b/components/backlog/backlog-card.tsx index 81d5c66..7a93910 100644 --- a/components/backlog/backlog-card.tsx +++ b/components/backlog/backlog-card.tsx @@ -2,6 +2,7 @@ import { forwardRef } from 'react' import { cn } from '@/lib/utils' +import { CodeBadge } from '@/components/shared/code-badge' export const PRIORITY_BORDER: Record = { 1: 'border-l-4 border-l-priority-critical', @@ -13,6 +14,7 @@ export const PRIORITY_BORDER: Record = { interface BacklogCardProps extends React.HTMLAttributes { title: string priority: number + code?: string | null isSelected?: boolean isDragging?: boolean badge?: React.ReactNode @@ -20,7 +22,7 @@ interface BacklogCardProps extends React.HTMLAttributes { } export const BacklogCard = forwardRef(function BacklogCard( - { title, priority, isSelected, isDragging, badge, actions, className, ...rest }, + { title, priority, code, isSelected, isDragging, badge, actions, className, ...rest }, ref ) { return ( @@ -37,7 +39,10 @@ export const BacklogCard = forwardRef(function )} {...rest} > -

{title}

+
+

{title}

+ {code && } +
{(badge || actions) && (
{badge}
diff --git a/components/backlog/pbi-list.tsx b/components/backlog/pbi-list.tsx index 30d2489..7e7cb20 100644 --- a/components/backlog/pbi-list.tsx +++ b/components/backlog/pbi-list.tsx @@ -49,6 +49,7 @@ const PRIORITY_COLORS: Record = { interface Pbi { id: string + code: string | null title: string priority: number description?: string | null @@ -92,6 +93,7 @@ function SortablePbiRow({ {...attributes} {...listeners} title={pbi.title} + code={pbi.code} priority={pbi.priority} isSelected={isSelected} isDragging={isDragging} diff --git a/components/backlog/story-panel.tsx b/components/backlog/story-panel.tsx index 4ff82f8..f6a804f 100644 --- a/components/backlog/story-panel.tsx +++ b/components/backlog/story-panel.tsx @@ -52,6 +52,7 @@ const STATUS_LABELS: Record = { export interface Story { id: string + code: string | null title: string description: string | null acceptance_criteria: string | null @@ -91,6 +92,7 @@ function SortableStoryBlock({ {...attributes} {...listeners} title={story.title} + code={story.code} priority={story.priority} isDragging={isDragging} onClick={onClick} diff --git a/components/dashboard/product-list.tsx b/components/dashboard/product-list.tsx index d9d0f19..4b41d54 100644 --- a/components/dashboard/product-list.tsx +++ b/components/dashboard/product-list.tsx @@ -5,11 +5,13 @@ import { useRouter } from 'next/navigation' import { useTransition } from 'react' import { toast } from 'sonner' import { Button } from '@/components/ui/button' +import { CodeBadge } from '@/components/shared/code-badge' import { restoreProductAction } from '@/actions/products' interface Product { id: string name: string + code: string | null description: string | null repo_url: string | null } @@ -61,9 +63,12 @@ export function ProductList({ products, isDemo, showArchived = false }: ProductL >
-

- {product.name} -

+
+ {product.code && } +

+ {product.name} +

+
{product.description && (

{product.description.slice(0, 80)}{product.description.length > 80 ? '…' : ''} diff --git a/components/solo/solo-board.tsx b/components/solo/solo-board.tsx index 149d7d5..60cddc4 100644 --- a/components/solo/solo-board.tsx +++ b/components/solo/solo-board.tsx @@ -22,7 +22,9 @@ export interface SoloTask { sort_order: number status: 'TO_DO' | 'IN_PROGRESS' | 'REVIEW' | 'DONE' story_id: string + story_code: string | null story_title: string + task_code: string | null } export interface SoloBoardProps { diff --git a/components/solo/solo-task-card.tsx b/components/solo/solo-task-card.tsx index 22ff289..6378010 100644 --- a/components/solo/solo-task-card.tsx +++ b/components/solo/solo-task-card.tsx @@ -3,6 +3,7 @@ import { useDraggable } from '@dnd-kit/core' import { CSS } from '@dnd-kit/utilities' import { cn } from '@/lib/utils' +import { CodeBadge } from '@/components/shared/code-badge' import type { SoloTask } from './solo-board' const PRIORITY_BORDER: Record = { @@ -39,8 +40,14 @@ export function SoloTaskCard({ task, isDemo, onClick }: SoloTaskCardProps) { )} {...(!isDemo ? { ...attributes, ...listeners } : {})} > -

{task.title}

-

{task.story_title}

+
+

{task.title}

+ {task.task_code && } +
+

+ {task.story_code && {task.story_code}} + {task.story_title} +

) } @@ -53,8 +60,14 @@ export function SoloTaskCardOverlay({ task }: { task: SoloTask }) { PRIORITY_BORDER[task.priority], )} > -

{task.title}

-

{task.story_title}

+
+

{task.title}

+ {task.task_code && } +
+

+ {task.story_code && {task.story_code}} + {task.story_title} +

) } diff --git a/components/solo/task-detail-dialog.tsx b/components/solo/task-detail-dialog.tsx index a325039..e2c4afe 100644 --- a/components/solo/task-detail-dialog.tsx +++ b/components/solo/task-detail-dialog.tsx @@ -77,11 +77,19 @@ function TaskDetailContent({ task, productId, isDemo, onClose }: TaskDetailConte {task.title} + {task.task_code && ( + + {task.task_code} + + )} {STATUS_LABELS[task.status]}
-

{task.story_title}

+

+ {task.story_code && {task.story_code}} + {task.story_title} +

{task.description && ( diff --git a/components/solo/unassigned-stories-sheet.tsx b/components/solo/unassigned-stories-sheet.tsx index b6ccbb5..6d84892 100644 --- a/components/solo/unassigned-stories-sheet.tsx +++ b/components/solo/unassigned-stories-sheet.tsx @@ -7,6 +7,7 @@ import { Sheet, SheetContent, SheetHeader, SheetTitle, } from '@/components/ui/sheet' import { DemoTooltip } from '@/components/shared/demo-tooltip' +import { CodeBadge } from '@/components/shared/code-badge' import { claimStoryAction } from '@/actions/stories' import { cn } from '@/lib/utils' @@ -20,6 +21,7 @@ export interface UnassignedStoryTask { export interface UnassignedStory { id: string + code: string | null title: string tasks: UnassignedStoryTask[] } @@ -119,7 +121,10 @@ function ClaimStoryRow({
-

{story.title}

+
+

{story.title}

+ {story.code && } +

{story.tasks.length} {story.tasks.length === 1 ? 'taak' : 'taken'}

diff --git a/components/sprint/sprint-backlog.tsx b/components/sprint/sprint-backlog.tsx index 7d5714b..bffc534 100644 --- a/components/sprint/sprint-backlog.tsx +++ b/components/sprint/sprint-backlog.tsx @@ -7,6 +7,7 @@ import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd- import { CSS } from '@dnd-kit/utilities' import { toast } from 'sonner' import { Badge } from '@/components/ui/badge' +import { CodeBadge } from '@/components/shared/code-badge' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent, @@ -35,6 +36,7 @@ const PRIORITY_LABELS: Record = { 1: 'Kritiek', 2: 'Hoog', 3: 'G export interface SprintStory { id: string + code: string | null title: string priority: number status: string @@ -51,6 +53,7 @@ export interface ProductMember { export interface PbiWithStories { id: string + code: string | null title: string stories: SprintStory[] } @@ -140,7 +143,10 @@ function SortableSprintRow({ )}
-

{story.title}

+
+

{story.title}

+ {story.code && } +
{PRIORITY_LABELS[story.priority]} @@ -338,7 +344,10 @@ function DraggablePbiStoryRow({ )}
-

{story.title}

+
+

{story.title}

+ {story.code && } +
{STATUS_LABELS[story.status]} @@ -387,6 +396,7 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on > {collapsed.has(pbi.id) ? '▶' : '▼'} {pbi.title} + {pbi.code && } {pbi.stories.length} @@ -400,7 +410,10 @@ export function SprintBacklogRight({ pbisWithStories, sprintStoryIds, isDemo, on >
-

{story.title}

+
+

{story.title}

+ {story.code && } +
{STATUS_LABELS[story.status]} diff --git a/components/sprint/sprint-board-client.tsx b/components/sprint/sprint-board-client.tsx index 9bb98ab..ce3e18d 100644 --- a/components/sprint/sprint-board-client.tsx +++ b/components/sprint/sprint-board-client.tsx @@ -228,6 +228,7 @@ export function SprintBoardClient({ selectedStoryId ? ( s.id === selectedStoryId)?.code ?? null} sprintId={sprintId} productId={productId} tasks={selectedTasks} diff --git a/components/sprint/sprint-header.tsx b/components/sprint/sprint-header.tsx index 5ae83a6..fcfa447 100644 --- a/components/sprint/sprint-header.tsx +++ b/components/sprint/sprint-header.tsx @@ -116,6 +116,7 @@ export function SprintHeader({ productId: _productId, productName, sprint, isDem
{sprintStories.map(story => (
+ {story.code && {story.code}} {story.title}
{!isDemo && ( -
+
@@ -150,7 +156,7 @@ function CreateSubmitButton() { return } -export function TaskList({ storyId, sprintId, productId: _productId, tasks, isDemo }: TaskListProps) { +export function TaskList({ storyId, storyCode, sprintId, productId: _productId, tasks, isDemo }: TaskListProps) { const { taskOrder, initTasks, reorderTasks, rollbackTasks } = useSprintStore() const [creating, setCreating] = useState(false) const [activeDragId, setActiveDragId] = useState(null) @@ -232,10 +238,11 @@ export function TaskList({ storyId, sprintId, productId: _productId, tasks, isDe onDragEnd={handleDragEnd} > t.id)} strategy={verticalListSortingStrategy}> - {orderedTasks.map(task => ( + {orderedTasks.map((task, idx) => ( handleStatusToggle(task)} onDelete={() => handleDelete(task.id)}