mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: extended the smartfileinput and add a global useFileViewer
This commit is contained in:
@@ -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>
|
||||
) : (
|
||||
|
||||
27
packages/ui-common/src/hooks/useFileViewer.tsx
Normal file
27
packages/ui-common/src/hooks/useFileViewer.tsx
Normal 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 };
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user