mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-01-08 06:44:05 -05:00
feat(frontend): add file input widget with variants and base64 support (#11533)
<!-- Clearly explain the need for these changes: --> This PR enhances the FileInput component to support multiple variants and modes, and integrates it into the form renderer as a file widget. The changes enable a more flexible file input experience with both server upload and local base64 conversion capabilities. ### Compact one <img width="354" height="91" alt="Screenshot 2025-12-03 at 8 05 51 PM" src="https://github.com/user-attachments/assets/1295a34f-4d9f-4a65-89f2-4b6516f176ff" /> <img width="386" height="96" alt="Screenshot 2025-12-03 at 8 06 11 PM" src="https://github.com/user-attachments/assets/3c10e350-8ddc-43ff-bf0a-68b23f8db394" /> ## Default one <img width="671" height="165" alt="Screenshot 2025-12-03 at 8 05 08 PM" src="https://github.com/user-attachments/assets/bd17c5f1-8cdb-4818-9850-a9e61252d8c1" /> <img width="656" height="141" alt="Screenshot 2025-12-03 at 8 05 21 PM" src="https://github.com/user-attachments/assets/1edf260f-6245-44b9-bd3e-dd962f497413" /> ### Changes 🏗️ #### FileInput Component Enhancements - **Added variant support**: Introduced `default` and `compact` variants - `default`: Full-featured UI with drag & drop, progress bar, and storage note - `compact`: Minimal inline design suitable for tight spaces like node inputs - **Added mode support**: Introduced `upload` and `base64` modes - `upload`: Uploads files to server (requires `onUploadFile` and `uploadProgress`) - `base64`: Converts files to base64 locally without server upload - **Improved type safety**: Refactored props using discriminated unions (`UploadModeProps | Base64ModeProps`) to ensure type-safe usage - **Enhanced file handling**: Added `getFileLabelFromValue` helper to extract file labels from base64 data URIs or file paths - **Better UX**: Added `showStorageNote` prop to control visibility of storage disclaimer #### FileWidget Integration - **Replaced legacy Input**: Migrated from legacy `Input` component to new `FileInput` component - **Smart variant selection**: Automatically selects `default` or `compact` variant based on form context size - **Base64 mode**: Uses base64 mode for form inputs, eliminating need for server uploads in builder context - **Improved accessibility**: Better disabled/readonly state handling with visual feedback #### Form Renderer Updates - **Disabled validation**: Added `noValidate={true}` and `liveValidate={false}` to prevent premature form validation #### Storybook Updates - **Expanded stories**: Added comprehensive stories for all variant/mode combinations: - `Default`: Default variant with upload mode - `Compact`: Compact variant with base64 mode - `CompactWithUpload`: Compact variant with upload mode - `DefaultWithBase64`: Default variant with base64 mode - **Improved documentation**: Updated component descriptions to clearly explain variants and modes ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Test FileInput component in Storybook with all variant/mode combinations - [x] Test file upload flow in default variant with upload mode - [x] Test base64 conversion in compact variant with base64 mode - [x] Test file widget in form renderer (node inputs) - [x] Test file type validation (accept prop) - [x] Test file size validation (maxFileSize prop) - [x] Test error handling for invalid files - [x] Test disabled and readonly states - [x] Test file clearing/removal functionality - [x] Verify compact variant renders correctly in tight spaces - [x] Verify default variant shows storage note only in upload mode - [x] Test drag & drop functionality in default variant
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import type { Meta } from "@storybook/nextjs";
|
||||
import { useState } from "react";
|
||||
import { FileInput } from "./FileInput";
|
||||
|
||||
const meta: Meta<typeof FileInput> = {
|
||||
const meta: Meta = {
|
||||
title: "Atoms/FileInput",
|
||||
component: FileInput,
|
||||
tags: ["autodocs"],
|
||||
@@ -11,26 +11,13 @@ const meta: Meta<typeof FileInput> = {
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"File upload input with progress and removable preview.\n\nProps:\n- accept: optional MIME/extensions filter (e.g. ['image/*', '.pdf']).\n- maxFileSize: optional maximum size in bytes; larger files are rejected with an inline error.",
|
||||
"File upload input with two variants and two modes.\n\n**Variants:**\n- `default`: Full-featured with drag & drop, progress bar, and storage note.\n- `compact`: Minimal inline design for tight spaces like node inputs.\n\n**Modes:**\n- `upload`: Uploads file to server (requires `onUploadFile` and `uploadProgress`).\n- `base64`: Converts file to base64 locally (no server upload).\n\n**Props:**\n- `accept`: optional MIME/extensions filter (e.g. ['image/*', '.pdf']).\n- `maxFileSize`: optional maximum size in bytes; larger files are rejected with an inline error.",
|
||||
},
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
onUploadFile: { action: "upload" },
|
||||
accept: {
|
||||
control: "object",
|
||||
description:
|
||||
"Optional accept filter. Supports MIME types (image/*) and extensions (.pdf).",
|
||||
},
|
||||
maxFileSize: {
|
||||
control: "number",
|
||||
description: "Optional maximum file size in bytes.",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
function mockUpload(file: File): Promise<{
|
||||
file_name: string;
|
||||
@@ -52,16 +39,16 @@ function mockUpload(file: File): Promise<{
|
||||
);
|
||||
}
|
||||
|
||||
export const Basic: Story = {
|
||||
export const Default = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"This example accepts images or PDFs only and limits size to 5MB. Oversized or disallowed file types show an inline error and do not upload.",
|
||||
"Default variant with upload mode. Full-featured with drag & drop dropzone, progress bar, and storage note. Accepts images or PDFs only and limits size to 5MB.",
|
||||
},
|
||||
},
|
||||
},
|
||||
render: function BasicStory() {
|
||||
render: function DefaultStory() {
|
||||
const [value, setValue] = useState<string>("");
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
@@ -79,6 +66,8 @@ export const Basic: Story = {
|
||||
return (
|
||||
<div className="w-[560px]">
|
||||
<FileInput
|
||||
variant="default"
|
||||
mode="upload"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onUploadFile={onUploadFile}
|
||||
@@ -90,3 +79,99 @@ export const Basic: Story = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Compact = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Compact variant with base64 mode. Minimal inline design suitable for node inputs. Converts file to base64 locally without server upload.",
|
||||
},
|
||||
},
|
||||
},
|
||||
render: function CompactStory() {
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="w-[300px]">
|
||||
<FileInput
|
||||
variant="compact"
|
||||
mode="base64"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
placeholder="Document"
|
||||
accept={["image/*", ".pdf"]}
|
||||
maxFileSize={5 * 1024 * 1024}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const CompactWithUpload = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Compact variant with upload mode. Useful when you need minimal UI but still want server uploads.",
|
||||
},
|
||||
},
|
||||
},
|
||||
render: function CompactUploadStory() {
|
||||
const [value, setValue] = useState<string>("");
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
async function onUploadFile(file: File) {
|
||||
setProgress(0);
|
||||
const interval = setInterval(() => {
|
||||
setProgress((p) => (p >= 100 ? 100 : p + 20));
|
||||
}, 80);
|
||||
const result = await mockUpload(file);
|
||||
clearInterval(interval);
|
||||
setProgress(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="w-[300px]">
|
||||
<FileInput
|
||||
variant="compact"
|
||||
mode="upload"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onUploadFile={onUploadFile}
|
||||
uploadProgress={progress}
|
||||
placeholder="Resume"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const DefaultWithBase64 = {
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story:
|
||||
"Default variant with base64 mode. Full-featured UI but converts to base64 locally instead of uploading.",
|
||||
},
|
||||
},
|
||||
},
|
||||
render: function DefaultBase64Story() {
|
||||
const [value, setValue] = useState<string>("");
|
||||
|
||||
return (
|
||||
<div className="w-[560px]">
|
||||
<FileInput
|
||||
variant="default"
|
||||
mode="base64"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
placeholder="Image"
|
||||
accept={["image/*"]}
|
||||
maxFileSize={2 * 1024 * 1024}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { FileTextIcon, TrashIcon, UploadIcon } from "@phosphor-icons/react";
|
||||
import { Cross2Icon } from "@radix-ui/react-icons";
|
||||
import { useRef, useState } from "react";
|
||||
import { Button } from "../Button/Button";
|
||||
import { formatFileSize, getFileLabel } from "./helpers";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Progress } from "../Progress/Progress";
|
||||
import { Text } from "../Text/Text";
|
||||
|
||||
type UploadFileResult = {
|
||||
file_name: string;
|
||||
@@ -12,26 +14,51 @@ type UploadFileResult = {
|
||||
file_uri: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
onUploadFile: (file: File) => Promise<UploadFileResult>;
|
||||
uploadProgress: number;
|
||||
value?: string; // file URI or empty
|
||||
placeholder?: string; // e.g. "Resume", "Document", etc.
|
||||
type FileInputVariant = "default" | "compact";
|
||||
|
||||
interface BaseProps {
|
||||
value?: string;
|
||||
placeholder?: string;
|
||||
onChange: (value: string) => void;
|
||||
className?: string;
|
||||
maxFileSize?: number; // bytes (optional)
|
||||
accept?: string | string[]; // input accept filter (optional)
|
||||
maxFileSize?: number;
|
||||
accept?: string | string[];
|
||||
variant?: FileInputVariant;
|
||||
showStorageNote?: boolean;
|
||||
}
|
||||
|
||||
export function FileInput({
|
||||
onUploadFile,
|
||||
uploadProgress,
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
maxFileSize,
|
||||
accept,
|
||||
}: Props) {
|
||||
interface UploadModeProps extends BaseProps {
|
||||
mode?: "upload";
|
||||
onUploadFile: (file: File) => Promise<UploadFileResult>;
|
||||
uploadProgress: number;
|
||||
}
|
||||
|
||||
interface Base64ModeProps extends BaseProps {
|
||||
mode: "base64";
|
||||
onUploadFile?: never;
|
||||
uploadProgress?: never;
|
||||
}
|
||||
|
||||
type Props = UploadModeProps | Base64ModeProps;
|
||||
|
||||
export function FileInput(props: Props) {
|
||||
const {
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
maxFileSize,
|
||||
accept,
|
||||
placeholder,
|
||||
variant = "default",
|
||||
showStorageNote = true,
|
||||
mode = "upload",
|
||||
} = props;
|
||||
|
||||
const onUploadFile =
|
||||
mode === "upload" ? (props as UploadModeProps).onUploadFile : undefined;
|
||||
const uploadProgress =
|
||||
mode === "upload" ? (props as UploadModeProps).uploadProgress : 0;
|
||||
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [uploadError, setUploadError] = useState<string | null>(null);
|
||||
const [fileInfo, setFileInfo] = useState<{
|
||||
@@ -40,7 +67,96 @@ export function FileInput({
|
||||
content_type: string;
|
||||
} | null>(null);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const storageNote =
|
||||
"Files are stored securely and will be automatically deleted at most 24 hours after upload.";
|
||||
|
||||
function acceptToString(a?: string | string[]) {
|
||||
if (!a) return "*/*";
|
||||
return Array.isArray(a) ? a.join(",") : a;
|
||||
}
|
||||
|
||||
function isAcceptedType(file: File, a?: string | string[]) {
|
||||
if (!a) return true;
|
||||
const list = Array.isArray(a) ? a : a.split(",").map((s) => s.trim());
|
||||
const fileType = file.type;
|
||||
const fileExt = file.name.includes(".")
|
||||
? `.${file.name.split(".").pop()}`.toLowerCase()
|
||||
: "";
|
||||
|
||||
for (const entry of list) {
|
||||
if (!entry) continue;
|
||||
const e = entry.toLowerCase();
|
||||
if (e.includes("/")) {
|
||||
const [main, sub] = e.split("/");
|
||||
const [fMain, fSub] = fileType.toLowerCase().split("/");
|
||||
if (!fMain || !fSub) continue;
|
||||
if (sub === "*") {
|
||||
if (main === fMain) return true;
|
||||
} else {
|
||||
if (e === fileType.toLowerCase()) return true;
|
||||
}
|
||||
} else if (e.startsWith(".")) {
|
||||
if (fileExt === e) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const getFileLabelFromValue = (val: string) => {
|
||||
if (val.startsWith("data:")) {
|
||||
const matches = val.match(/^data:([^;]+);/);
|
||||
if (matches?.[1]) {
|
||||
const mimeParts = matches[1].split("/");
|
||||
if (mimeParts.length > 1) {
|
||||
return `${mimeParts[1].toUpperCase()} file`;
|
||||
}
|
||||
return `${matches[1]} file`;
|
||||
}
|
||||
} else {
|
||||
const pathParts = val.split(".");
|
||||
if (pathParts.length > 1) {
|
||||
const ext = pathParts.pop();
|
||||
if (ext) return `${ext.toUpperCase()} file`;
|
||||
}
|
||||
}
|
||||
return "File";
|
||||
};
|
||||
|
||||
const processFileBase64 = (file: File) => {
|
||||
setIsUploading(true);
|
||||
setUploadError(null);
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const base64String = e.target?.result as string;
|
||||
setFileInfo({
|
||||
name: file.name,
|
||||
size: file.size,
|
||||
content_type: file.type || "application/octet-stream",
|
||||
});
|
||||
onChange(base64String);
|
||||
setIsUploading(false);
|
||||
};
|
||||
reader.onerror = () => {
|
||||
setUploadError("Failed to read file");
|
||||
setIsUploading(false);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
};
|
||||
|
||||
const uploadFile = async (file: File) => {
|
||||
if (mode === "base64") {
|
||||
processFileBase64(file);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!onUploadFile) {
|
||||
setUploadError("Upload handler not provided");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
setUploadError(null);
|
||||
|
||||
@@ -53,7 +169,6 @@ export function FileInput({
|
||||
content_type: result.content_type,
|
||||
});
|
||||
|
||||
// Set the file URI as the value
|
||||
onChange(result.file_uri);
|
||||
} catch (error) {
|
||||
console.error("Upload failed:", error);
|
||||
@@ -87,43 +202,104 @@ export function FileInput({
|
||||
if (file) uploadFile(file);
|
||||
};
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const storageNote =
|
||||
"Files are stored securely and will be automatically deleted at most 24 hours after upload.";
|
||||
|
||||
function acceptToString(a?: string | string[]) {
|
||||
if (!a) return "*/*";
|
||||
return Array.isArray(a) ? a.join(",") : a;
|
||||
}
|
||||
|
||||
function isAcceptedType(file: File, a?: string | string[]) {
|
||||
if (!a) return true;
|
||||
const list = Array.isArray(a) ? a : a.split(",").map((s) => s.trim());
|
||||
const fileType = file.type; // e.g. image/png
|
||||
const fileExt = file.name.includes(".")
|
||||
? `.${file.name.split(".").pop()}`.toLowerCase()
|
||||
: "";
|
||||
|
||||
for (const entry of list) {
|
||||
if (!entry) continue;
|
||||
const e = entry.toLowerCase();
|
||||
if (e.includes("/")) {
|
||||
// MIME type, support wildcards like image/*
|
||||
const [main, sub] = e.split("/");
|
||||
const [fMain, fSub] = fileType.toLowerCase().split("/");
|
||||
if (!fMain || !fSub) continue;
|
||||
if (sub === "*") {
|
||||
if (main === fMain) return true;
|
||||
} else {
|
||||
if (e === fileType.toLowerCase()) return true;
|
||||
}
|
||||
} else if (e.startsWith(".")) {
|
||||
// Extension match
|
||||
if (fileExt === e) return true;
|
||||
}
|
||||
const handleClear = () => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
return false;
|
||||
onChange("");
|
||||
setFileInfo(null);
|
||||
};
|
||||
|
||||
const displayName = placeholder || "File";
|
||||
|
||||
if (variant === "compact") {
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-1.5", className)}>
|
||||
<div className="nodrag flex flex-col gap-1.5">
|
||||
{isUploading ? (
|
||||
<div className="flex flex-col gap-1.5 rounded-md border border-blue-200 bg-blue-50 p-2 dark:border-blue-800 dark:bg-blue-950">
|
||||
<div className="flex items-center gap-2">
|
||||
<UploadIcon className="h-4 w-4 animate-pulse text-blue-600 dark:text-blue-400" />
|
||||
<Text
|
||||
variant="small"
|
||||
className="text-blue-700 dark:text-blue-300"
|
||||
>
|
||||
{mode === "base64" ? "Processing..." : "Uploading..."}
|
||||
</Text>
|
||||
{mode === "upload" && (
|
||||
<Text
|
||||
variant="small-medium"
|
||||
className="ml-auto text-blue-600 dark:text-blue-400"
|
||||
>
|
||||
{Math.round(uploadProgress)}%
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
{mode === "upload" && (
|
||||
<Progress value={uploadProgress} className="h-1 w-full" />
|
||||
)}
|
||||
</div>
|
||||
) : value ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-1 items-center gap-2 rounded-xlarge border border-gray-300 bg-gray-50 p-2 dark:border-gray-600 dark:bg-gray-800">
|
||||
<FileTextIcon className="h-4 w-4 flex-shrink-0 text-gray-600 dark:text-gray-400" />
|
||||
|
||||
<Text
|
||||
variant="small-medium"
|
||||
className="truncate text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
{fileInfo
|
||||
? getFileLabel(fileInfo.name, fileInfo.content_type)
|
||||
: getFileLabelFromValue(value)}
|
||||
</Text>
|
||||
{fileInfo && (
|
||||
<Text
|
||||
variant="small"
|
||||
className="text-gray-500 dark:text-gray-400"
|
||||
>
|
||||
{formatFileSize(fileInfo.size)}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
className="h-7 w-7 min-w-0 flex-shrink-0 border-zinc-300 p-0 text-gray-500 hover:text-red-600 dark:text-gray-400 dark:hover:text-red-500"
|
||||
onClick={handleClear}
|
||||
>
|
||||
<Cross2Icon className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="small"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="flex-1 border-zinc-300 text-xs"
|
||||
disabled={isUploading}
|
||||
>
|
||||
<UploadIcon className="mr-1.5 h-3.5 w-3.5" />
|
||||
{`Upload ${displayName}`}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept={acceptToString(accept)}
|
||||
onChange={handleFileChange}
|
||||
className="hidden"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
{uploadError && (
|
||||
<Text variant="small" className="text-red-600 dark:text-red-400">
|
||||
{uploadError}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -134,15 +310,23 @@ export function FileInput({
|
||||
<div className="agpt-border-input flex min-h-14 w-full flex-col justify-center rounded-xl bg-zinc-50 p-4 text-sm">
|
||||
<div className="mb-2 flex items-center gap-2">
|
||||
<UploadIcon className="h-5 w-5 text-blue-600" />
|
||||
<span className="text-gray-700">Uploading...</span>
|
||||
<span className="text-gray-500">
|
||||
{Math.round(uploadProgress)}%
|
||||
<span className="text-gray-700">
|
||||
{mode === "base64" ? "Processing..." : "Uploading..."}
|
||||
</span>
|
||||
{mode === "upload" && (
|
||||
<span className="text-gray-500">
|
||||
{Math.round(uploadProgress)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<Progress value={uploadProgress} className="w-full" />
|
||||
{mode === "upload" && (
|
||||
<Progress value={uploadProgress} className="w-full" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{storageNote}</p>
|
||||
{showStorageNote && mode === "upload" && (
|
||||
<p className="text-xs text-gray-500">{storageNote}</p>
|
||||
)}
|
||||
</div>
|
||||
) : value ? (
|
||||
<div className="space-y-2">
|
||||
@@ -154,24 +338,20 @@ export function FileInput({
|
||||
<span className="font-normal text-black">
|
||||
{fileInfo
|
||||
? getFileLabel(fileInfo.name, fileInfo.content_type)
|
||||
: "File"}
|
||||
: getFileLabelFromValue(value)}
|
||||
</span>
|
||||
<span>{fileInfo ? formatFileSize(fileInfo.size) : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
<TrashIcon
|
||||
className="h-5 w-5 cursor-pointer text-black"
|
||||
onClick={() => {
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = "";
|
||||
}
|
||||
onChange("");
|
||||
setFileInfo(null);
|
||||
}}
|
||||
onClick={handleClear}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{storageNote}</p>
|
||||
{showStorageNote && mode === "upload" && (
|
||||
<p className="text-xs text-gray-500">{storageNote}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
@@ -196,7 +376,9 @@ export function FileInput({
|
||||
<div className="text-sm text-red-600">Error: {uploadError}</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs text-gray-500">{storageNote}</p>
|
||||
{showStorageNote && mode === "upload" && (
|
||||
<p className="text-xs text-gray-500">{storageNote}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -45,6 +45,8 @@ export const FormRenderer = ({
|
||||
onChange={handleChange}
|
||||
uiSchema={uiSchema}
|
||||
formData={initialValues}
|
||||
noValidate={true}
|
||||
liveValidate={false}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,33 +1,27 @@
|
||||
import { WidgetProps } from "@rjsf/utils";
|
||||
import { Input } from "@/components/__legacy__/ui/input";
|
||||
import { FileInput } from "@/components/atoms/FileInput/FileInput";
|
||||
|
||||
export const FileWidget = (props: WidgetProps) => {
|
||||
const { onChange, multiple = false, disabled, readonly, id } = props;
|
||||
const { onChange, disabled, readonly, value, schema, formContext } = props;
|
||||
|
||||
// TODO: It's temporary solution for file input, will complete it follow up prs
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files;
|
||||
if (!files || files.length === 0) {
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
const { size } = formContext || {};
|
||||
|
||||
const file = files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
onChange(e.target?.result);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
const displayName = schema?.title || "File";
|
||||
|
||||
const handleChange = (fileUri: string) => {
|
||||
onChange(fileUri);
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
id={id}
|
||||
type="file"
|
||||
multiple={multiple}
|
||||
disabled={disabled || readonly}
|
||||
<FileInput
|
||||
variant={size === "large" ? "default" : "compact"}
|
||||
mode="base64"
|
||||
value={value}
|
||||
placeholder={displayName}
|
||||
onChange={handleChange}
|
||||
className="rounded-full"
|
||||
className={
|
||||
disabled || readonly ? "pointer-events-none opacity-50" : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user