feat(uat-batch-24): click-to-preview on EntityFolderView + HubRootView Files

Completes the click-to-preview sweep across all file-row surfaces. The
filename cells in entity-folder-view.tsx (entity-scoped Files panel)
and hub-root-view.tsx (Documents Hub root "Recent files") were the
last two non-clickable surfaces — both now wrap the filename in a
button that opens FilePreviewDialog directly, matching the FileGrid
and DocumentList pattern shipped in 52342ee.

HubRootFile shape extended to include mimeType (already returned by
the /api/v1/files endpoint via the buildListQuery passthrough) so the
preview dialog can branch on image vs PDF without a second request.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 19:21:11 +02:00
parent a263a202d9
commit ded16f4a5b
2 changed files with 53 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import { ClipboardSignature, FileText, Eye } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { AggregatedSection } from './aggregated-section'; import { AggregatedSection } from './aggregated-section';
import { SigningDetailsDialog } from './signing-details-dialog'; import { SigningDetailsDialog } from './signing-details-dialog';
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
import { useAggregatedFiles, useAggregatedWorkflows } from '@/hooks/use-aggregated-listing'; import { useAggregatedFiles, useAggregatedWorkflows } from '@/hooks/use-aggregated-listing';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
import type { import type {
@@ -35,6 +36,11 @@ function mapWorkflowStatus(status: string): StatusPillStatus {
export function EntityFolderView({ portSlug, entityType, entityId }: Props) { export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
const [detailsId, setDetailsId] = useState<string | null>(null); const [detailsId, setDetailsId] = useState<string | null>(null);
const [previewFile, setPreviewFile] = useState<{
id: string;
name: string;
mimeType: string | null;
} | null>(null);
// Hook data is the bare AggregatedGroup<T>[] array (hooks unwrap the API envelope). // Hook data is the bare AggregatedGroup<T>[] array (hooks unwrap the API envelope).
const { data: workflowGroups = [], isLoading: workflowsLoading } = useAggregatedWorkflows( const { data: workflowGroups = [], isLoading: workflowsLoading } = useAggregatedWorkflows(
@@ -76,7 +82,14 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
const signedFromDocumentId = f.signedFromDocumentId; const signedFromDocumentId = f.signedFromDocumentId;
return ( return (
<div className="flex items-center justify-between gap-2 text-sm"> <div className="flex items-center justify-between gap-2 text-sm">
<span className="truncate">{f.filename}</span> <button
type="button"
className="truncate text-left hover:text-brand hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 rounded-sm"
onClick={() => setPreviewFile({ id: f.id, name: f.filename, mimeType: f.mimeType })}
aria-label={`Preview ${f.filename}`}
>
{f.filename}
</button>
<div className="flex items-center gap-2 text-xs text-muted-foreground tabular-nums"> <div className="flex items-center gap-2 text-xs text-muted-foreground tabular-nums">
<span>{new Date(f.createdAt).toLocaleDateString('en-GB')}</span> <span>{new Date(f.createdAt).toLocaleDateString('en-GB')}</span>
{signedFromDocumentId ? ( {signedFromDocumentId ? (
@@ -101,6 +114,16 @@ export function EntityFolderView({ portSlug, entityType, entityId }: Props) {
open={Boolean(detailsId)} open={Boolean(detailsId)}
onOpenChange={(open) => !open && setDetailsId(null)} onOpenChange={(open) => !open && setDetailsId(null)}
/> />
<FilePreviewDialog
open={Boolean(previewFile)}
onOpenChange={(open) => {
if (!open) setPreviewFile(null);
}}
fileId={previewFile?.id}
fileName={previewFile?.name}
mimeType={previewFile?.mimeType ?? undefined}
/>
</div> </div>
); );
} }

View File

@@ -1,10 +1,12 @@
'use client'; 'use client';
import { useState } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { FileText, ClipboardSignature } from 'lucide-react'; import { FileText, ClipboardSignature } from 'lucide-react';
import { usePaginatedQuery } from '@/hooks/use-paginated-query'; import { usePaginatedQuery } from '@/hooks/use-paginated-query';
import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill'; import { StatusPill, type StatusPillStatus } from '@/components/ui/status-pill';
import { FilePreviewDialog } from '@/components/files/file-preview-dialog';
interface HubRootDoc { interface HubRootDoc {
id: string; id: string;
@@ -17,6 +19,7 @@ interface HubRootDoc {
interface HubRootFile { interface HubRootFile {
id: string; id: string;
filename: string; filename: string;
mimeType: string | null;
createdAt: string; createdAt: string;
} }
@@ -36,6 +39,12 @@ const STATUS_PILL_MAP: Record<string, StatusPillStatus> = {
}; };
export function HubRootView({ portSlug }: Props) { export function HubRootView({ portSlug }: Props) {
const [previewFile, setPreviewFile] = useState<{
id: string;
name: string;
mimeType: string | null;
} | null>(null);
const { data: workflows, isLoading: workflowsLoading } = usePaginatedQuery<HubRootDoc>({ const { data: workflows, isLoading: workflowsLoading } = usePaginatedQuery<HubRootDoc>({
queryKey: ['documents', 'hub-root', 'workflows'], queryKey: ['documents', 'hub-root', 'workflows'],
endpoint: '/api/v1/documents?tab=in_progress', endpoint: '/api/v1/documents?tab=in_progress',
@@ -87,7 +96,16 @@ export function HubRootView({ portSlug }: Props) {
<ul className="divide-y"> <ul className="divide-y">
{filesData.map((f) => ( {filesData.map((f) => (
<li key={f.id} className="flex items-center justify-between px-3 py-2 text-sm"> <li key={f.id} className="flex items-center justify-between px-3 py-2 text-sm">
<span className="truncate">{f.filename}</span> <button
type="button"
className="truncate text-left hover:text-brand hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 rounded-sm"
onClick={() =>
setPreviewFile({ id: f.id, name: f.filename, mimeType: f.mimeType })
}
aria-label={`Preview ${f.filename}`}
>
{f.filename}
</button>
<span className="text-xs text-muted-foreground tabular-nums"> <span className="text-xs text-muted-foreground tabular-nums">
{new Date(f.createdAt).toLocaleDateString('en-GB')} {new Date(f.createdAt).toLocaleDateString('en-GB')}
</span> </span>
@@ -96,6 +114,16 @@ export function HubRootView({ portSlug }: Props) {
</ul> </ul>
)} )}
</section> </section>
<FilePreviewDialog
open={Boolean(previewFile)}
onOpenChange={(open) => {
if (!open) setPreviewFile(null);
}}
fileId={previewFile?.id}
fileName={previewFile?.name}
mimeType={previewFile?.mimeType ?? undefined}
/>
</div> </div>
); );
} }