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>
) : (