Files
vibe-kanban/frontend/src/components/panels/DiffsPanel.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

239 lines
6.9 KiB
TypeScript

import { useDiffStream } from '@/hooks/useDiffStream';
import { useMemo, useCallback, useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { Loader } from '@/components/ui/loader';
import { Button } from '@/components/ui/button';
import DiffViewSwitch from '@/components/diff-view-switch';
import DiffCard from '@/components/DiffCard';
import { useDiffSummary } from '@/hooks/useDiffSummary';
import { NewCardHeader } from '@/components/ui/new-card';
import { ChevronsUp, ChevronsDown } from 'lucide-react';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import type { TaskAttempt, Diff } from 'shared/types';
import GitOperations, {
type GitOperationsInputs,
} from '@/components/tasks/Toolbar/GitOperations.tsx';
interface DiffsPanelProps {
selectedAttempt: TaskAttempt | null;
gitOps?: GitOperationsInputs;
}
export function DiffsPanel({ selectedAttempt, gitOps }: DiffsPanelProps) {
const { t } = useTranslation('tasks');
const [loading, setLoading] = useState(true);
const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set());
const [hasInitialized, setHasInitialized] = useState(false);
const { diffs, error } = useDiffStream(selectedAttempt?.id ?? null, true);
const { fileCount, added, deleted } = useDiffSummary(
selectedAttempt?.id ?? null
);
useEffect(() => {
setLoading(true);
setHasInitialized(false);
}, [selectedAttempt?.id]);
useEffect(() => {
setLoading(true);
}, [selectedAttempt?.id]);
useEffect(() => {
if (diffs.length > 0 && loading) {
setLoading(false);
}
}, [diffs, loading]);
// If no diffs arrive within 3 seconds, stop showing the spinner
useEffect(() => {
if (!loading) return;
const timer = setTimeout(() => {
if (diffs.length === 0) {
setLoading(false);
}
}, 3000);
return () => clearTimeout(timer);
}, [loading, diffs.length]);
// Default-collapse certain change kinds on first load only
useEffect(() => {
if (diffs.length === 0) return;
if (hasInitialized) return; // only run once per attempt
const kindsToCollapse = new Set([
'deleted',
'renamed',
'copied',
'permissionChange',
]);
const initial = new Set(
diffs
.filter((d) => kindsToCollapse.has(d.change))
.map((d, i) => d.newPath || d.oldPath || String(i))
);
if (initial.size > 0) setCollapsedIds(initial);
setHasInitialized(true);
}, [diffs, hasInitialized]);
const ids = useMemo(() => {
return diffs.map((d, i) => d.newPath || d.oldPath || String(i));
}, [diffs]);
const toggle = useCallback((id: string) => {
setCollapsedIds((prev) => {
const next = new Set(prev);
next.has(id) ? next.delete(id) : next.add(id);
return next;
});
}, []);
const allCollapsed = collapsedIds.size === diffs.length;
const handleCollapseAll = useCallback(() => {
setCollapsedIds(allCollapsed ? new Set() : new Set(ids));
}, [allCollapsed, ids]);
if (error) {
return (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 m-4">
<div className="text-red-800 text-sm">
{t('diff.errorLoadingDiff', { error })}
</div>
</div>
);
}
return (
<DiffsPanelContent
diffs={diffs}
fileCount={fileCount}
added={added}
deleted={deleted}
collapsedIds={collapsedIds}
allCollapsed={allCollapsed}
handleCollapseAll={handleCollapseAll}
toggle={toggle}
selectedAttempt={selectedAttempt}
gitOps={gitOps}
loading={loading}
t={t}
/>
);
}
interface DiffsPanelContentProps {
diffs: Diff[];
fileCount: number;
added: number;
deleted: number;
collapsedIds: Set<string>;
allCollapsed: boolean;
handleCollapseAll: () => void;
toggle: (id: string) => void;
selectedAttempt: TaskAttempt | null;
gitOps?: GitOperationsInputs;
loading: boolean;
t: (key: string, params?: Record<string, unknown>) => string;
}
function DiffsPanelContent({
diffs,
fileCount,
added,
deleted,
collapsedIds,
allCollapsed,
handleCollapseAll,
toggle,
selectedAttempt,
gitOps,
loading,
t,
}: DiffsPanelContentProps) {
return (
<div className="h-full flex flex-col relative">
{diffs.length > 0 && (
<NewCardHeader
className="sticky top-0 z-10"
actions={
<>
<DiffViewSwitch />
<div className="h-4 w-px bg-border" />
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="icon"
onClick={handleCollapseAll}
aria-pressed={allCollapsed}
aria-label={
allCollapsed
? t('diff.expandAll')
: t('diff.collapseAll')
}
>
{allCollapsed ? (
<ChevronsDown className="h-4 w-4" />
) : (
<ChevronsUp className="h-4 w-4" />
)}
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{allCollapsed ? t('diff.expandAll') : t('diff.collapseAll')}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</>
}
>
<div className="flex items-center">
<span
className="text-sm text-muted-foreground whitespace-nowrap"
aria-live="polite"
>
{t('diff.filesChanged', { count: fileCount })}{' '}
<span className="text-green-600 dark:text-green-500">
+{added}
</span>{' '}
<span className="text-red-600 dark:text-red-500">-{deleted}</span>
</span>
</div>
</NewCardHeader>
)}
{gitOps && selectedAttempt && (
<div className="px-3">
<GitOperations selectedAttempt={selectedAttempt} {...gitOps} />
</div>
)}
<div className="flex-1 overflow-y-auto px-3">
{loading ? (
<div className="flex items-center justify-center h-full">
<Loader />
</div>
) : diffs.length === 0 ? (
<div className="flex items-center justify-center h-full text-sm text-muted-foreground">
{t('diff.noChanges')}
</div>
) : (
diffs.map((diff, idx) => {
const id = diff.newPath || diff.oldPath || String(idx);
return (
<DiffCard
key={id}
diff={diff}
expanded={!collapsedIds.has(id)}
onToggle={() => toggle(id)}
selectedAttempt={selectedAttempt}
/>
);
})
)}
</div>
</div>
);
}