feat: extended the smartfileinput and add a global useFileViewer

This commit is contained in:
Nathnael
2026-07-03 06:57:22 +00:00
parent 1611c6be22
commit 33ca30461e
3 changed files with 113 additions and 21 deletions

View File

@@ -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();
* <SmartFileInput ... onViewFile={view} />
* {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 <File className={cn("text-slate-400", className)} />;
}
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 (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
onViewFile({ name: f.name, url: f.url, mimeType: f.mimeType });
}}
className={cn(
"relative z-10 text-left text-xs text-primary hover:underline truncate max-w-xs",
className,
)}
>
{label}
</button>
);
}
return (
<a
href={f.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
className={cn(
"relative z-10 text-xs text-primary hover:underline truncate max-w-xs",
className,
)}
>
{label}
</a>
);
}
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 && (
<div className="flex flex-col gap-1 basis-full">
{existingForField.map((f, idx) => (
<a
<ExistingFileLink
key={`${f.url}-${idx}`}
href={f.url}
target="_blank"
rel="noopener noreferrer"
className="text-xs text-primary hover:underline truncate max-w-xs"
>
{f.name}
</a>
file={f}
onViewFile={onViewFile}
/>
))}
</div>
)}
@@ -488,19 +558,12 @@ export function SmartFileInput({
{existingForField.length > 0 ? (
<div className="mt-0.5 flex flex-col gap-0.5">
{existingForField.map((f, idx) => (
<a
<ExistingFileLink
key={`${f.url}-${idx}`}
href={f.url}
target="_blank"
rel="noopener noreferrer"
onClick={(e) => 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)})`
: ""}
</a>
file={f}
onViewFile={onViewFile}
showSize
/>
))}
</div>
) : (

View File

@@ -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();
* <Button onClick={() => view({ name, url, mimeType })}>View</Button>
* {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<ViewableFile | null>(null);
const view = useCallback((f: ViewableFile) => setFile(f), []);
const close = useCallback(() => setFile(null), []);
const viewer = (
<FileViewerModal open={file !== null} file={file} onClose={close} />
);
return { view, close, viewer };
}

View File

@@ -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";