mirror of
https://github.com/BradGroux/veritas-kanban.git
synced 2026-10-10 21:57:53 +00:00
RF-20: Frontend performance optimization — memoization for diff components
- Add memo + useMemo to DiffViewer, FileDiffView, DiffHunkView, FileTree - Add useCallback for event handlers to prevent re-renders - Fix unused typeIconName variable in TaskCard - All diff viewer components now properly memoized to prevent unnecessary re-renders when comments change
This commit is contained in:
parent
b03630be5c
commit
d7aeeeaeb2
8 changed files with 100 additions and 37 deletions
|
|
@ -600,3 +600,5 @@
|
|||
{"type":"task.status_changed","taskId":"task_20260128_Qh60Ao","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_IMzc-x6hnSTj","timestamp":"2026-01-28T12:29:48.687Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_Qh60Ao","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_SljUoovW7DVZ","timestamp":"2026-01-28T12:31:58.645Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_VnO-L8","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_pCeJ0lWaPH9N","timestamp":"2026-01-28T12:32:22.407Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_VnO-L8","project":"veritas-kanban","status":"done","previousStatus":"in-progress","id":"evt_gTOhwBV5lGo0","timestamp":"2026-01-28T12:33:34.327Z"}
|
||||
{"type":"task.status_changed","taskId":"task_20260128_ksQUvs","project":"veritas-kanban","status":"in-progress","previousStatus":"todo","id":"evt_WyBRK_mNhgAl","timestamp":"2026-01-28T12:34:14.344Z"}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,37 @@
|
|||
[
|
||||
{
|
||||
"id": "activity_1769603654345_x6t8uc64f",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_ksQUvs",
|
||||
"taskTitle": "RF-20: Frontend Performance Optimization",
|
||||
"details": {
|
||||
"from": "todo",
|
||||
"status": "in-progress"
|
||||
},
|
||||
"timestamp": "2026-01-28T12:34:14.345Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769603622409_5thybkvj9",
|
||||
"type": "comment_added",
|
||||
"taskId": "task_20260128_VnO-L8",
|
||||
"taskTitle": "RF-12: Extract Shared WebSocket Hook",
|
||||
"details": {
|
||||
"author": "Veritas",
|
||||
"preview": "Created useWebSocket.ts with connection lifecycle,..."
|
||||
},
|
||||
"timestamp": "2026-01-28T12:33:42.409Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769603614328_ci4tk6yfp",
|
||||
"type": "status_changed",
|
||||
"taskId": "task_20260128_VnO-L8",
|
||||
"taskTitle": "RF-12: Extract Shared WebSocket Hook",
|
||||
"details": {
|
||||
"from": "in-progress",
|
||||
"status": "done"
|
||||
},
|
||||
"timestamp": "2026-01-28T12:33:34.329Z"
|
||||
},
|
||||
{
|
||||
"id": "activity_1769603542408_0wffse0lg",
|
||||
"type": "status_changed",
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { memo, useState, useMemo, useCallback } from 'react';
|
||||
import { useDiffSummary } from '@/hooks/useDiff';
|
||||
import { FileTree } from './diff/FileTree';
|
||||
import { FileDiffView } from './diff/FileDiffView';
|
||||
|
|
@ -12,12 +12,16 @@ interface DiffViewerProps {
|
|||
onRemoveComment: (commentId: string) => void;
|
||||
}
|
||||
|
||||
export function DiffViewer({ task, onAddComment, onRemoveComment }: DiffViewerProps) {
|
||||
export const DiffViewer = memo(function DiffViewer({ task, onAddComment, onRemoveComment }: DiffViewerProps) {
|
||||
const hasWorktree = !!task.git?.worktreePath;
|
||||
const { data: summary, isLoading, error } = useDiffSummary(task.id, hasWorktree);
|
||||
const [selectedFile, setSelectedFile] = useState<string | null>(null);
|
||||
|
||||
const comments = task.reviewComments || [];
|
||||
const comments = useMemo(() => task.reviewComments || [], [task.reviewComments]);
|
||||
|
||||
const handleSelectFile = useCallback((file: string | null) => {
|
||||
setSelectedFile(file);
|
||||
}, []);
|
||||
|
||||
if (!hasWorktree) {
|
||||
return (
|
||||
|
|
@ -79,7 +83,7 @@ export function DiffViewer({ task, onAddComment, onRemoveComment }: DiffViewerPr
|
|||
<FileTree
|
||||
files={summary.files}
|
||||
selectedFile={selectedFile}
|
||||
onSelectFile={setSelectedFile}
|
||||
onSelectFile={handleSelectFile}
|
||||
comments={comments}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -105,4 +109,4 @@ export function DiffViewer({ task, onAddComment, onRemoveComment }: DiffViewerPr
|
|||
</div>
|
||||
</FeatureErrorBoundary>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
|
@ -40,7 +41,7 @@ const priorityColors: Record<TaskPriority, string> = {
|
|||
low: 'bg-slate-500/20 text-slate-400',
|
||||
};
|
||||
|
||||
export function TaskCard({ task, isDragging, onClick, isSelected, isBlocked, blockerTitles }: TaskCardProps) {
|
||||
export const TaskCard = memo(function TaskCard({ task, isDragging, onClick, isSelected, isBlocked, blockerTitles }: TaskCardProps) {
|
||||
const { taskTypes, projects, sprints } = useTaskConfig();
|
||||
const {
|
||||
attributes,
|
||||
|
|
@ -79,22 +80,34 @@ export function TaskCard({ task, isDragging, onClick, isSelected, isBlocked, blo
|
|||
|
||||
const isAgentRunning = task.attempt?.status === 'running';
|
||||
|
||||
// Get type info dynamically
|
||||
const typeConfig = taskTypes.find(t => t.id === task.type);
|
||||
const typeIconName = typeConfig?.icon || 'Code';
|
||||
const typeLabel = typeConfig?.label || task.type;
|
||||
const TypeIconComponent = getTypeIcon(typeIconName);
|
||||
const typeColor = getTypeColor(taskTypes, task.type);
|
||||
// Memoize type info
|
||||
const { typeLabel, TypeIconComponent, typeColor } = useMemo(() => {
|
||||
const typeConfig = taskTypes.find(t => t.id === task.type);
|
||||
const iconName = typeConfig?.icon || 'Code';
|
||||
return {
|
||||
typeLabel: typeConfig?.label || task.type,
|
||||
TypeIconComponent: getTypeIcon(iconName),
|
||||
typeColor: getTypeColor(taskTypes, task.type),
|
||||
};
|
||||
}, [taskTypes, task.type]);
|
||||
|
||||
// Get project info dynamically
|
||||
const projectColor = task.project ? getProjectColor(projects, task.project) : 'bg-muted';
|
||||
const projectLabel = task.project ? getProjectLabel(projects, task.project) : '';
|
||||
// Memoize project info
|
||||
const { projectColor, projectLabel } = useMemo(() => ({
|
||||
projectColor: task.project ? getProjectColor(projects, task.project) : 'bg-muted',
|
||||
projectLabel: task.project ? getProjectLabel(projects, task.project) : '',
|
||||
}), [projects, task.project]);
|
||||
|
||||
// Subtask progress
|
||||
const subtasks = task.subtasks || [];
|
||||
const subtaskTotal = subtasks.length;
|
||||
const subtaskCompleted = subtasks.filter(s => s.completed).length;
|
||||
const allSubtasksDone = subtaskTotal > 0 && subtaskCompleted === subtaskTotal;
|
||||
// Memoize subtask progress
|
||||
const { subtaskTotal, subtaskCompleted, allSubtasksDone } = useMemo(() => {
|
||||
const subtasks = task.subtasks || [];
|
||||
const total = subtasks.length;
|
||||
const completed = subtasks.filter(s => s.completed).length;
|
||||
return {
|
||||
subtaskTotal: total,
|
||||
subtaskCompleted: completed,
|
||||
allSubtasksDone: total > 0 && completed === total,
|
||||
};
|
||||
}, [task.subtasks]);
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
|
|
@ -265,4 +278,4 @@ export function TaskCard({ task, isDragging, onClick, isSelected, isBlocked, blo
|
|||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { memo } from 'react';
|
||||
import { DiffLineView } from './DiffLine';
|
||||
import type { DiffHunk } from '@/lib/api';
|
||||
import type { ReviewComment } from '@veritas-kanban/shared';
|
||||
|
|
@ -12,7 +13,7 @@ interface DiffHunkProps {
|
|||
onRemoveComment: (commentId: string) => void;
|
||||
}
|
||||
|
||||
export function DiffHunkView({
|
||||
export const DiffHunkView = memo(function DiffHunkView({
|
||||
hunk,
|
||||
comments,
|
||||
addingCommentAtLine,
|
||||
|
|
@ -43,4 +44,4 @@ export function DiffHunkView({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import { Plus, MessageSquare } from 'lucide-react';
|
||||
import { CommentInput, CommentDisplay } from './ReviewComment';
|
||||
import type { DiffLine } from '@/lib/api';
|
||||
|
|
@ -14,7 +15,7 @@ interface DiffLineProps {
|
|||
onRemoveComment: (commentId: string) => void;
|
||||
}
|
||||
|
||||
export function DiffLineView({
|
||||
export const DiffLineView = memo(function DiffLineView({
|
||||
line,
|
||||
comments,
|
||||
addingCommentAtLine,
|
||||
|
|
@ -24,7 +25,10 @@ export function DiffLineView({
|
|||
onRemoveComment,
|
||||
}: DiffLineProps) {
|
||||
const lineNumber = line.newNumber || line.oldNumber;
|
||||
const lineComments = comments.filter(c => c.line === lineNumber);
|
||||
const lineComments = useMemo(
|
||||
() => comments.filter(c => c.line === lineNumber),
|
||||
[comments, lineNumber]
|
||||
);
|
||||
const isAddingHere = addingCommentAtLine === lineNumber;
|
||||
|
||||
return (
|
||||
|
|
@ -94,4 +98,4 @@ export function DiffLineView({
|
|||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useState } from 'react';
|
||||
import { memo, useState, useMemo, useCallback } from 'react';
|
||||
import { useFileDiff } from '@/hooks/useDiff';
|
||||
import { DiffHunkView } from './DiffHunk';
|
||||
import {
|
||||
|
|
@ -29,7 +29,7 @@ interface FileDiffViewProps {
|
|||
onRemoveComment: (commentId: string) => void;
|
||||
}
|
||||
|
||||
export function FileDiffView({
|
||||
export const FileDiffView = memo(function FileDiffView({
|
||||
taskId,
|
||||
filePath,
|
||||
comments,
|
||||
|
|
@ -39,9 +39,12 @@ export function FileDiffView({
|
|||
const { data: diff, isLoading, error } = useFileDiff(taskId, filePath);
|
||||
const [addingCommentAtLine, setAddingCommentAtLine] = useState<number | null>(null);
|
||||
|
||||
const fileComments = comments.filter(c => c.file === filePath);
|
||||
const fileComments = useMemo(
|
||||
() => comments.filter(c => c.file === filePath),
|
||||
[comments, filePath]
|
||||
);
|
||||
|
||||
const handleSubmitComment = (content: string) => {
|
||||
const handleSubmitComment = useCallback((content: string) => {
|
||||
if (addingCommentAtLine === null) return;
|
||||
|
||||
const comment: ReviewComment = {
|
||||
|
|
@ -54,7 +57,9 @@ export function FileDiffView({
|
|||
|
||||
onAddComment(comment);
|
||||
setAddingCommentAtLine(null);
|
||||
};
|
||||
}, [addingCommentAtLine, filePath, onAddComment]);
|
||||
|
||||
const handleCancelComment = useCallback(() => setAddingCommentAtLine(null), []);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
|
|
@ -110,11 +115,11 @@ export function FileDiffView({
|
|||
addingCommentAtLine={addingCommentAtLine}
|
||||
onStartAddComment={setAddingCommentAtLine}
|
||||
onSubmitComment={handleSubmitComment}
|
||||
onCancelComment={() => setAddingCommentAtLine(null)}
|
||||
onCancelComment={handleCancelComment}
|
||||
onRemoveComment={onRemoveComment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { memo, useMemo } from 'react';
|
||||
import {
|
||||
FileCode,
|
||||
FilePlus,
|
||||
|
|
@ -23,11 +24,11 @@ interface FileTreeProps {
|
|||
comments: ReviewComment[];
|
||||
}
|
||||
|
||||
export function FileTree({ files, selectedFile, onSelectFile, comments }: FileTreeProps) {
|
||||
const commentsByFile = comments.reduce((acc, c) => {
|
||||
export const FileTree = memo(function FileTree({ files, selectedFile, onSelectFile, comments }: FileTreeProps) {
|
||||
const commentsByFile = useMemo(() => comments.reduce((acc, c) => {
|
||||
acc[c.file] = (acc[c.file] || 0) + 1;
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
}, {} as Record<string, number>), [comments]);
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
|
|
@ -61,4 +62,4 @@ export function FileTree({ files, selectedFile, onSelectFile, comments }: FileTr
|
|||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue