agent-ecosystem/src/renderer/components/team/editor/EditorImagePreview.tsx
iliya 2ceed41e00 fix: resolve all CI lint errors and flaky test
- Fix React hooks violations: ref updates during render (useDraftPersistence,
  useChipDraftPersistence, useAttachments), setState in effects across 15+
  components, useCallback self-reference TDZ in useResizableColumns
- Fix TypeScript lint: remove unnecessary type assertions, replace inline
  import() annotations with direct imports, remove unused variables/imports
- Fix SonarJS issues: prefer-regexp-exec, slow-regex in SubagentResolver,
  no-misleading-array-reverse in TeamProvisioningService, use-type-alias
  in ClaudeLogsSection, variable shadowing in ChangeExtractorService
- Fix accessibility: associate labels with controls in filter popovers
- Fix template expression safety: wrap unknown errors with String()
- Fix flaky FileWatcher test: floor instanceCreatedAt to second granularity
  to match filesystem birthtimeMs resolution on Linux
- Replace TODO comments with NOTE where features are intentionally disabled
- Remove unused leadContextByTeam from TeamDetailView store selector

62 files changed across main process, renderer, shared types, and hooks.
All 1646 tests pass, typecheck clean, 0 lint errors.
2026-03-05 21:09:45 +02:00

137 lines
4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Inline image preview for the project editor.
*
* Loads binary file as base64 data URL via IPC, displays centered image
* with checkerboard background for transparency, metadata, and lightbox on click.
*/
import { useCallback, useEffect, useRef, useState } from 'react';
import { ImageLightbox } from '@renderer/components/team/attachments/ImageLightbox';
import { Button } from '@renderer/components/ui/button';
import { useStore } from '@renderer/store';
import { Loader2 } from 'lucide-react';
import { EditorBinaryPlaceholder } from './EditorBinaryPlaceholder';
interface EditorImagePreviewProps {
filePath: string;
fileName: string;
size: number;
}
export const EditorImagePreview = ({
filePath,
fileName,
size,
}: EditorImagePreviewProps): React.ReactElement => {
const projectPath = useStore((s) => s.editorProjectPath);
const [dataUrl, setDataUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [lightboxOpen, setLightboxOpen] = useState(false);
const [dimensions, setDimensions] = useState<{ w: number; h: number } | null>(null);
const imgRef = useRef<HTMLImageElement>(null);
// Reset state when filePath changes
useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect -- intentional sync on prop change
setLoading(true);
setError(null);
setDataUrl(null);
setDimensions(null);
setLightboxOpen(false);
}, [filePath]);
useEffect(() => {
let cancelled = false;
window.electronAPI.editor
.readBinaryPreview(filePath)
.then((result) => {
if (cancelled) return;
setDataUrl(`data:${result.mimeType};base64,${result.base64}`);
})
.catch((err: Error) => {
if (cancelled) return;
setError(err.message);
})
.finally(() => {
if (!cancelled) setLoading(false);
});
return () => {
cancelled = true;
};
}, [filePath]);
const handleImageLoad = useCallback(() => {
const img = imgRef.current;
if (img) {
setDimensions({ w: img.naturalWidth, h: img.naturalHeight });
}
}, []);
const handleOpenExternal = useCallback((): void => {
window.electronAPI.openPath(filePath, projectPath ?? undefined).catch(console.error);
}, [filePath, projectPath]);
const sizeFormatted =
size < 1024
? `${size} B`
: size < 1024 * 1024
? `${(size / 1024).toFixed(1)} KB`
: `${(size / 1024 / 1024).toFixed(1)} MB`;
if (loading) {
return (
<div className="flex h-full flex-col items-center justify-center gap-3 text-text-muted">
<Loader2 className="size-8 animate-spin opacity-40" />
<p className="text-xs">Loading preview</p>
</div>
);
}
if (error || !dataUrl) {
return <EditorBinaryPlaceholder filePath={filePath} fileName={fileName} size={size} />;
}
return (
<div className="flex h-full flex-col items-center justify-center gap-4 p-6">
<button
type="button"
className="checkerboard-bg flex max-h-[60vh] max-w-[80%] cursor-zoom-in items-center justify-center overflow-hidden rounded-lg border border-border-subtle p-1"
onClick={() => setLightboxOpen(true)}
aria-label="Open full-size preview"
>
<img
ref={imgRef}
src={dataUrl}
alt={fileName}
className="max-h-[60vh] object-contain"
onLoad={handleImageLoad}
draggable={false}
/>
</button>
<p className="text-xs text-text-muted">
{fileName}
{dimensions ? `${dimensions.w}×${dimensions.h}` : ''}
{`${sizeFormatted}`}
</p>
<div className="flex gap-2">
<Button variant="outline" size="sm" onClick={handleOpenExternal}>
Open in System Viewer
</Button>
</div>
<ImageLightbox
open={lightboxOpen}
onClose={() => setLightboxOpen(false)}
src={dataUrl}
alt={fileName}
/>
</div>
);
};