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";
|
} from "lucide-react";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
import { Button } from "../button";
|
import { Button } from "../button";
|
||||||
|
import type { ViewableFile } from "../FileViewer";
|
||||||
|
|
||||||
export interface SmartFileInputProps {
|
export interface SmartFileInputProps {
|
||||||
/** The settings object containing features and their upload fields config. */
|
/** The settings object containing features and their upload fields config. */
|
||||||
@@ -35,8 +36,18 @@ export interface SmartFileInputProps {
|
|||||||
*/
|
*/
|
||||||
existingFiles?: Record<
|
existingFiles?: Record<
|
||||||
string,
|
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 state for the entire file input group. */
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
/** Display variant style. Default is "default" (large dropzone). Minimal renders a compact upload button. */
|
/** 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)} />;
|
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({
|
export function SmartFileInput({
|
||||||
file,
|
file,
|
||||||
value,
|
value,
|
||||||
@@ -87,6 +160,7 @@ export function SmartFileInput({
|
|||||||
errors,
|
errors,
|
||||||
uploadedKeys,
|
uploadedKeys,
|
||||||
existingFiles,
|
existingFiles,
|
||||||
|
onViewFile,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
className,
|
className,
|
||||||
@@ -433,15 +507,11 @@ export function SmartFileInput({
|
|||||||
{existingForField.length > 0 && (
|
{existingForField.length > 0 && (
|
||||||
<div className="flex flex-col gap-1 basis-full">
|
<div className="flex flex-col gap-1 basis-full">
|
||||||
{existingForField.map((f, idx) => (
|
{existingForField.map((f, idx) => (
|
||||||
<a
|
<ExistingFileLink
|
||||||
key={`${f.url}-${idx}`}
|
key={`${f.url}-${idx}`}
|
||||||
href={f.url}
|
file={f}
|
||||||
target="_blank"
|
onViewFile={onViewFile}
|
||||||
rel="noopener noreferrer"
|
/>
|
||||||
className="text-xs text-primary hover:underline truncate max-w-xs"
|
|
||||||
>
|
|
||||||
{f.name}
|
|
||||||
</a>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -488,19 +558,12 @@ export function SmartFileInput({
|
|||||||
{existingForField.length > 0 ? (
|
{existingForField.length > 0 ? (
|
||||||
<div className="mt-0.5 flex flex-col gap-0.5">
|
<div className="mt-0.5 flex flex-col gap-0.5">
|
||||||
{existingForField.map((f, idx) => (
|
{existingForField.map((f, idx) => (
|
||||||
<a
|
<ExistingFileLink
|
||||||
key={`${f.url}-${idx}`}
|
key={`${f.url}-${idx}`}
|
||||||
href={f.url}
|
file={f}
|
||||||
target="_blank"
|
onViewFile={onViewFile}
|
||||||
rel="noopener noreferrer"
|
showSize
|
||||||
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>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</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,
|
ViewableFile,
|
||||||
} from "./components/FileViewer";
|
} from "./components/FileViewer";
|
||||||
|
|
||||||
|
export { useFileViewer } from "./hooks/useFileViewer";
|
||||||
|
|
||||||
export { OperationDatePicker } from "./components/OperationDatePicker";
|
export { OperationDatePicker } from "./components/OperationDatePicker";
|
||||||
export type { OperationDatePickerProps } from "./components/OperationDatePicker";
|
export type { OperationDatePickerProps } from "./components/OperationDatePicker";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user