From 33ca30461ed86ab0796b4871ff601c40fbb429ff Mon Sep 17 00:00:00 2001 From: Nathnael Date: Fri, 3 Jul 2026 06:57:22 +0000 Subject: [PATCH] feat: extended the smartfileinput and add a global useFileViewer --- .../src/components/SmartFileInput/index.tsx | 105 ++++++++++++++---- .../ui-common/src/hooks/useFileViewer.tsx | 27 +++++ packages/ui-common/src/index.ts | 2 + 3 files changed, 113 insertions(+), 21 deletions(-) create mode 100644 packages/ui-common/src/hooks/useFileViewer.tsx diff --git a/packages/ui-common/src/components/SmartFileInput/index.tsx b/packages/ui-common/src/components/SmartFileInput/index.tsx index d39e431c7..0da3e2f37 100644 --- a/packages/ui-common/src/components/SmartFileInput/index.tsx +++ b/packages/ui-common/src/components/SmartFileInput/index.tsx @@ -11,6 +11,7 @@ import { } from "lucide-react"; import { cn } from "../../lib/utils"; import { Button } from "../button"; +import type { ViewableFile } from "../FileViewer"; export interface SmartFileInputProps { /** The settings object containing features and their upload fields config. */ @@ -35,8 +36,18 @@ export interface SmartFileInputProps { */ existingFiles?: Record< string, - { name: string; url: string; size?: number }[] + { name: string; url: string; size?: number; mimeType?: string | null }[] >; + /** + * When provided, already-uploaded files render as buttons that call this with + * the file instead of opening a new browser tab. Wire it to `useFileViewer`'s + * `view` to preview documents inline: + * + * const { view, viewer } = useFileViewer(); + * + * {viewer} + */ + onViewFile?: (file: ViewableFile) => void; /** Disabled state for the entire file input group. */ disabled?: boolean; /** Display variant style. Default is "default" (large dropzone). Minimal renders a compact upload button. */ @@ -80,6 +91,68 @@ function FileIcon({ name, className }: { name: string; className?: string }) { return ; } +type ExistingFile = { + name: string; + url: string; + size?: number; + mimeType?: string | null; +}; + +/** + * A single already-uploaded file. Renders a click-to-view button when + * `onViewFile` is set (inline preview via the FileViewer), otherwise a plain + * new-tab anchor. + */ +function ExistingFileLink({ + file: f, + onViewFile, + className, + showSize = false, +}: { + file: ExistingFile; + onViewFile?: (file: ViewableFile) => void; + className?: string; + showSize?: boolean; +}) { + const label = + showSize && typeof f.size === "number" + ? `${f.name} (${formatBytes(f.size)})` + : f.name; + + if (onViewFile) { + return ( + + ); + } + + return ( + e.stopPropagation()} + className={cn( + "relative z-10 text-xs text-primary hover:underline truncate max-w-xs", + className, + )} + > + {label} + + ); +} + export function SmartFileInput({ file, value, @@ -87,6 +160,7 @@ export function SmartFileInput({ errors, uploadedKeys, existingFiles, + onViewFile, disabled = false, variant = "default", className, @@ -433,15 +507,11 @@ export function SmartFileInput({ {existingForField.length > 0 && (
{existingForField.map((f, idx) => ( - - {f.name} - + file={f} + onViewFile={onViewFile} + /> ))}
)} @@ -488,19 +558,12 @@ export function SmartFileInput({ {existingForField.length > 0 ? (
{existingForField.map((f, idx) => ( - e.stopPropagation()} - className="relative z-10 text-xs text-primary hover:underline truncate max-w-xs" - > - {f.name} - {typeof f.size === "number" - ? ` (${formatBytes(f.size)})` - : ""} - + file={f} + onViewFile={onViewFile} + showSize + /> ))}
) : ( diff --git a/packages/ui-common/src/hooks/useFileViewer.tsx b/packages/ui-common/src/hooks/useFileViewer.tsx new file mode 100644 index 000000000..68269f7cf --- /dev/null +++ b/packages/ui-common/src/hooks/useFileViewer.tsx @@ -0,0 +1,27 @@ +import { useCallback, useState } from "react"; +import { FileViewerModal, type ViewableFile } from "../components/FileViewer"; + +/** + * Drives a single shared {@link FileViewerModal} for a page. Call `view(file)` + * from any file row to open the document inline (pdf / image / video / office / + * text); render `viewer` once near the page root. + * + * const { view, viewer } = useFileViewer(); + * + * {viewer} + * + * Pass `view` straight into `SmartFileInput`'s `onViewFile` prop to make its + * already-uploaded files open in the viewer instead of a new tab. + */ +export function useFileViewer() { + const [file, setFile] = useState(null); + + const view = useCallback((f: ViewableFile) => setFile(f), []); + const close = useCallback(() => setFile(null), []); + + const viewer = ( + + ); + + return { view, close, viewer }; +} diff --git a/packages/ui-common/src/index.ts b/packages/ui-common/src/index.ts index 2e8779076..43c39d962 100644 --- a/packages/ui-common/src/index.ts +++ b/packages/ui-common/src/index.ts @@ -20,6 +20,8 @@ export type { ViewableFile, } from "./components/FileViewer"; +export { useFileViewer } from "./hooks/useFileViewer"; + export { OperationDatePicker } from "./components/OperationDatePicker"; export type { OperationDatePickerProps } from "./components/OperationDatePicker";