Files
vibe-kanban/frontend/src/components/DiffCard.tsx
Louis Knight-Webb 41376eba94 Batch C: Fix medium-priority 'any' in UI components (vibe-kanban) (#1312)
* ##  Batch C Complete

All medium-priority TypeScript `any` types have been fixed across 7 files:

1. **[markdown-renderer.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/ui/markdown-renderer.tsx)** - Replaced 11 `any` with `React.ComponentProps<'tag'>`
2. **[ExecutorConfigForm.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/ExecutorConfigForm.tsx)** - Used `unknown` for form data, `IChangeEvent<unknown>` for handlers
3. **[FormTemplate.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/rjsf/templates/FormTemplate.tsx)** - Replaced with `React.PropsWithChildren`
4. **[ArrayFieldTemplate.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/rjsf/templates/ArrayFieldTemplate.tsx)** - Created minimal `ArrayItemWithButtons` type for RJSF v6 beta
5. **[DiffsPanel.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/panels/DiffsPanel.tsx)** - Used `Diff[]` and typed `t` function properly
6. **[DiffCard.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/DiffCard.tsx)** - Used existing `ReviewComment` type from context
7. **[useGitOperations.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/hooks/useGitOperations.ts)** - Properly narrowed `unknown` errors with type guards

`pnpm run check` passes 

* Cleanup script changes for task attempt 5fe86537-3bc1-4d78-96c0-95347fc7c05f

* Excellent! The build succeeds. Now let's create a summary:

##  Batch C Complete + RJSF Upgrade

Successfully fixed all medium-priority TypeScript `any` types **and** upgraded RJSF to stable 6.1.1:

### Type Fixes (7 files)
1. **[markdown-renderer.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/ui/markdown-renderer.tsx)** - Replaced 11 `any` with `React.ComponentProps<'tag'>`
2. **[ExecutorConfigForm.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/ExecutorConfigForm.tsx)** - Used `unknown` and `IChangeEvent<unknown>`
3. **[FormTemplate.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/rjsf/templates/FormTemplate.tsx)** - Used `React.PropsWithChildren`
4. **[ArrayFieldTemplate.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/rjsf/templates/ArrayFieldTemplate.tsx)** - Migrated to stable RJSF v6 API with `ArrayFieldItemTemplate`
5. **[DiffsPanel.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/panels/DiffsPanel.tsx)** - Used `Diff[]` and proper `t` function type
6. **[DiffCard.tsx](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/components/DiffCard.tsx)** - Used `ReviewComment` type from context
7. **[useGitOperations.ts](file:///private/var/folders/m1/9q_ct1913z10v6wbnv54j25r0000gn/T/vibe-kanban/worktrees/5fe8-batch-c-fix-medi/frontend/src/hooks/useGitOperations.ts)** - Properly narrowed `unknown` with type guards

### Dependency Upgrades
- `@rjsf/core`: 6.0.0-beta.11 → **6.1.1** (stable)
- `@rjsf/utils`: 6.0.0-beta.11 → **6.1.1** (stable)
- `@rjsf/validator-ajv8`: 6.0.0-beta.11 → **6.1.1** (stable)
- `@rjsf/shadcn`: 6.0.0-beta.10 → **6.1.1** (stable)

### RJSF Migration
Updated to v6 stable API:
- Removed workaround type (no longer needed)
- Added `ArrayFieldItemTemplate` for proper item rendering
- Updated to use `onRemoveItem` callback instead of deprecated `onDropIndexClick`

All checks pass 

* Cleanup script changes for task attempt 5fe86537-3bc1-4d78-96c0-95347fc7c05f
2025-11-17 22:35:20 +00:00

339 lines
9.7 KiB
TypeScript

import { Diff } from 'shared/types';
import { DiffModeEnum, DiffView, SplitSide } from '@git-diff-view/react';
import { generateDiffFile, type DiffFile } from '@git-diff-view/file';
import { useMemo } from 'react';
import { useUserSystem } from '@/components/config-provider';
import { getHighLightLanguageFromPath } from '@/utils/extToLanguage';
import { getActualTheme } from '@/utils/theme';
import { stripLineEnding } from '@/utils/string';
import { Button } from '@/components/ui/button';
import {
ChevronRight,
ChevronUp,
Trash2,
ArrowLeftRight,
FilePlus2,
PencilLine,
Copy,
Key,
ExternalLink,
MessageSquare,
} from 'lucide-react';
import '@/styles/diff-style-overrides.css';
import { attemptsApi } from '@/lib/api';
import type { TaskAttempt } from 'shared/types';
import {
useReview,
type ReviewDraft,
type ReviewComment,
} from '@/contexts/ReviewProvider';
import { CommentWidgetLine } from '@/components/diff/CommentWidgetLine';
import { ReviewCommentRenderer } from '@/components/diff/ReviewCommentRenderer';
import {
useDiffViewMode,
useIgnoreWhitespaceDiff,
} from '@/stores/useDiffViewStore';
import { useProject } from '@/contexts/project-context';
type Props = {
diff: Diff;
expanded: boolean;
onToggle: () => void;
selectedAttempt: TaskAttempt | null;
};
function labelAndIcon(diff: Diff) {
const c = diff.change;
if (c === 'deleted') return { label: 'Deleted', Icon: Trash2 };
if (c === 'renamed') return { label: 'Renamed', Icon: ArrowLeftRight };
if (c === 'added')
return { label: undefined as string | undefined, Icon: FilePlus2 };
if (c === 'copied') return { label: 'Copied', Icon: Copy };
if (c === 'permissionChange')
return { label: 'Permission Changed', Icon: Key };
return { label: undefined as string | undefined, Icon: PencilLine };
}
function readPlainLine(
diffFile: DiffFile | null,
lineNumber: number,
side: SplitSide
) {
if (!diffFile) return undefined;
try {
const rawLine =
side === SplitSide.old
? diffFile.getOldPlainLine(lineNumber)
: diffFile.getNewPlainLine(lineNumber);
if (rawLine?.value === undefined) return undefined;
return stripLineEnding(rawLine.value);
} catch (error) {
console.error('Failed to read line content for review comment', error);
return undefined;
}
}
export default function DiffCard({
diff,
expanded,
onToggle,
selectedAttempt,
}: Props) {
const { config } = useUserSystem();
const theme = getActualTheme(config?.theme);
const { comments, drafts, setDraft } = useReview();
const globalMode = useDiffViewMode();
const ignoreWhitespace = useIgnoreWhitespaceDiff();
const { projectId } = useProject();
const oldName = diff.oldPath || undefined;
const newName = diff.newPath || oldName || 'unknown';
const oldLang =
getHighLightLanguageFromPath(oldName || newName || '') || 'plaintext';
const newLang =
getHighLightLanguageFromPath(newName || oldName || '') || 'plaintext';
const { label, Icon } = labelAndIcon(diff);
const isOmitted = !!diff.contentOmitted;
// Build a diff from raw contents so the viewer can expand beyond hunks
const oldContentSafe = diff.oldContent || '';
const newContentSafe = diff.newContent || '';
const isContentEqual = oldContentSafe === newContentSafe;
const diffOptions = useMemo(
() => (ignoreWhitespace ? { ignoreWhitespace: true as const } : undefined),
[ignoreWhitespace]
);
const diffFile = useMemo(() => {
if (isContentEqual || isOmitted) return null;
try {
const oldFileName = oldName || newName || 'unknown';
const newFileName = newName || oldName || 'unknown';
const file = generateDiffFile(
oldFileName,
oldContentSafe,
newFileName,
newContentSafe,
oldLang,
newLang,
diffOptions
);
file.initRaw();
return file;
} catch (e) {
console.error('Failed to build diff for view', e);
return null;
}
}, [
isContentEqual,
isOmitted,
oldName,
newName,
oldLang,
newLang,
oldContentSafe,
newContentSafe,
diffOptions,
]);
const add = isOmitted
? (diff.additions ?? 0)
: (diffFile?.additionLength ?? 0);
const del = isOmitted
? (diff.deletions ?? 0)
: (diffFile?.deletionLength ?? 0);
// Review functionality
const filePath = newName || oldName || 'unknown';
const commentsForFile = useMemo(
() => comments.filter((c) => c.filePath === filePath),
[comments, filePath]
);
// Transform comments to git-diff-view extendData format
const extendData = useMemo(() => {
const oldFileData: Record<string, { data: ReviewComment }> = {};
const newFileData: Record<string, { data: ReviewComment }> = {};
commentsForFile.forEach((comment) => {
const lineKey = String(comment.lineNumber);
if (comment.side === SplitSide.old) {
oldFileData[lineKey] = { data: comment };
} else {
newFileData[lineKey] = { data: comment };
}
});
return {
oldFile: oldFileData,
newFile: newFileData,
};
}, [commentsForFile]);
const handleAddWidgetClick = (lineNumber: number, side: SplitSide) => {
const widgetKey = `${filePath}-${side}-${lineNumber}`;
const codeLine = readPlainLine(diffFile, lineNumber, side);
const draft: ReviewDraft = {
filePath,
side,
lineNumber,
text: '',
...(codeLine !== undefined ? { codeLine } : {}),
};
setDraft(widgetKey, draft);
};
const renderWidgetLine = (props: {
side: SplitSide;
lineNumber: number;
onClose: () => void;
}) => {
const widgetKey = `${filePath}-${props.side}-${props.lineNumber}`;
const draft = drafts[widgetKey];
if (!draft) return null;
return (
<CommentWidgetLine
draft={draft}
widgetKey={widgetKey}
onSave={props.onClose}
onCancel={props.onClose}
projectId={projectId}
/>
);
};
const renderExtendLine = (lineData: { data: ReviewComment }) => {
return (
<ReviewCommentRenderer comment={lineData.data} projectId={projectId} />
);
};
// Title row
const title = (
<p
className="text-xs font-mono overflow-x-auto flex-1"
style={{ color: 'hsl(var(--muted-foreground) / 0.7)' }}
>
<Icon className="h-3 w-3 inline mr-2" aria-hidden />
{label && <span className="mr-2">{label}</span>}
{diff.change === 'renamed' && oldName ? (
<span className="inline-flex items-center gap-2">
<span>{oldName}</span>
<span aria-hidden></span>
<span>{newName}</span>
</span>
) : (
<span>{newName}</span>
)}
<span className="ml-3" style={{ color: 'hsl(var(--console-success))' }}>
+{add}
</span>
<span className="ml-2" style={{ color: 'hsl(var(--console-error))' }}>
-{del}
</span>
{commentsForFile.length > 0 && (
<span className="ml-3 inline-flex items-center gap-1 px-2 py-0.5 text-xs bg-primary/10 text-primary rounded">
<MessageSquare className="h-3 w-3" />
{commentsForFile.length}
</span>
)}
</p>
);
const handleOpenInIDE = async () => {
if (!selectedAttempt?.id) return;
try {
const openPath = newName || oldName;
const response = await attemptsApi.openEditor(
selectedAttempt.id,
undefined,
openPath || undefined
);
// If a URL is returned, open it in a new window/tab
if (response.url) {
window.open(response.url, '_blank');
}
} catch (err) {
console.error('Failed to open file in IDE:', err);
}
};
const expandable = true;
return (
<div className="my-4 border">
<div className="flex items-center px-4 py-2">
{expandable && (
<Button
variant="ghost"
size="sm"
onClick={onToggle}
className="h-6 w-6 p-0 mr-2"
title={expanded ? 'Collapse' : 'Expand'}
aria-expanded={expanded}
>
{expanded ? (
<ChevronUp className="h-3 w-3" />
) : (
<ChevronRight className="h-3 w-3" />
)}
</Button>
)}
{title}
<Button
variant="ghost"
size="sm"
onClick={(e) => {
e.stopPropagation();
handleOpenInIDE();
}}
className="h-6 w-6 p-0 ml-2"
title="Open in IDE"
disabled={diff.change === 'deleted'}
>
<ExternalLink className="h-3 w-3" aria-hidden />
</Button>
</div>
{expanded && diffFile && (
<div>
<DiffView
diffFile={diffFile}
diffViewWrap={false}
diffViewTheme={theme}
diffViewHighlight
diffViewMode={
globalMode === 'split' ? DiffModeEnum.Split : DiffModeEnum.Unified
}
diffViewFontSize={12}
diffViewAddWidget
onAddWidgetClick={handleAddWidgetClick}
renderWidgetLine={renderWidgetLine}
extendData={extendData}
renderExtendLine={renderExtendLine}
/>
</div>
)}
{expanded && !diffFile && (
<div
className="px-4 pb-4 text-xs font-mono"
style={{ color: 'hsl(var(--muted-foreground) / 0.9)' }}
>
{isOmitted
? 'Content omitted due to file size. Open in editor to view.'
: isContentEqual
? diff.change === 'renamed'
? 'File renamed with no content changes.'
: diff.change === 'permissionChange'
? 'File permission changed.'
: 'No content changes to display.'
: 'Failed to render diff for this file.'}
</div>
)}
</div>
);
}