import * as React from "react"; import { useState, useEffect } from "react"; import styled from "styled-components"; import type { ControlProps } from "./BaseControl"; import BaseControl from "./BaseControl"; import type { ControlType } from "constants/PropertyControlConstants"; import type { WrappedFieldInputProps, WrappedFieldMetaProps } from "redux-form"; import { Field, getFormValues } from "redux-form"; import { Button, Tag, Text, toast } from "design-system"; import type { AppState } from "@appsmith/reducers"; import type { Datasource } from "entities/Datasource"; import type { Action } from "entities/Action"; import { connect } from "react-redux"; import PluginsApi from "api/PluginApi"; import { get, isArray } from "lodash"; import { formatFileSize } from "./utils"; import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors"; const HiddenFileInput = styled.input` visibility: hidden; `; interface ConnectProps { pluginId?: string; currentFiles: FileMetadata[]; workpaceId: string; } export type MultipleFilePickerControlProps = ControlProps & { allowedFileTypes?: string[]; maxFileSizeInBytes?: number; uploadFilesToTrigger?: boolean; pluginId?: string; config?: { uploadToTrigger?: boolean; params?: Record; }; buttonLabel?: string; } & ConnectProps; type FilePickerProps = MultipleFilePickerControlProps & { input?: WrappedFieldInputProps; meta?: WrappedFieldMetaProps; disabled?: boolean; onChange: (event: any) => void; maxUploadSize: number; }; interface FileMetadata { id: string; name: string; size: number; mimetype: string; base64Content?: string; } export interface FileUploadResponse { data: { files: { id: string; name: string; size: number; mimetype: string; }[]; }; } function FilePicker(props: FilePickerProps) { const { buttonLabel = "Select Files", config: { params: uploadParams = {}, uploadToTrigger = false } = {}, } = props; const [uploading, setUploading] = useState(false); const [uploadedFiles, setUploadedFiles] = useState([]); const fileInputRef = React.useRef(null); const uploadFilesToTriggerApi = async ( files: File[], ): Promise => { if (!props.pluginId) return []; try { const response = await PluginsApi.uploadFiles(props.pluginId, files, { ...uploadParams, workspaceId: props.workpaceId, }); if ("trigger" in response.data) { const { data: { files }, } = response.data.trigger as FileUploadResponse; return files; } else { return []; } } catch (e) { toast.show("Error uploading files", { kind: "error" }); return []; } }; const validateFileSizes = (files: File[]) => { const { maxFileSizeInBytes = -1 } = props; if (maxFileSizeInBytes === -1) return true; let totalSize = 0; uploadedFiles.forEach((file) => { totalSize += file.size; }); files.forEach((file) => { totalSize += file.size; }); if (totalSize > maxFileSizeInBytes) { toast.show( `Total file sizes execceds the maximum allowed size of ${formatFileSize( maxFileSizeInBytes, )}`, { kind: "error", }, ); clearInput(); return false; } return true; }; const clearInput = () => { if (fileInputRef.current) { fileInputRef.current.value = ""; } }; const getBase64Content = async (file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(); } }; }); }; const handleFileChange: React.ChangeEventHandler = async ( event, ) => { const files = event.target.files; if (!files) return; if (files.length === 0) return; const filesArray = Array.from(files); if (!validateFileSizes(filesArray)) { clearInput(); return; } setUploading(true); let newFiles: FileMetadata[] = []; if (uploadToTrigger) { newFiles = await uploadFilesToTriggerApi(filesArray); } else { filesArray.forEach(async (file) => { const base64Content = await getBase64Content(file); newFiles.push({ id: file.name, name: file.name, size: file.size, mimetype: file.type, base64Content, }); }); } setUploadedFiles((prev) => [...prev, ...newFiles]); setUploading(false); clearInput(); }; const onRemoveFile = (fileId: string) => { setUploadedFiles((prev) => prev.filter((uploadedFile) => uploadedFile.id !== fileId), ); }; useEffect(() => { props.input?.onChange(uploadedFiles); }, [uploadedFiles]); useEffect(() => { if (props.currentFiles && props.currentFiles.length > 0) { setUploadedFiles(props.currentFiles); } }, [props.currentFiles]); const allowedFileTypesProps = isArray(props.allowedFileTypes) ? props.allowedFileTypes.join(",") : undefined; return (
{uploadedFiles.map((file) => ( { onRemoveFile(file.id); }} size="md" >
{file.name} ({formatFileSize(file.size)})
))}
); } class MultipleFilePickerControl extends BaseControl { constructor(props: MultipleFilePickerControlProps) { super(props); this.state = { isOpen: false, }; } render() { const { configProperty, disabled } = this.props; return ( ); } getControlType(): ControlType { return "MULTIPLE_FILE_PICKER"; } } export interface FilePickerComponentState { isOpen: boolean; } const mapStateToProps = ( state: AppState, ownProps: MultipleFilePickerControlProps, ): ConnectProps => { const formValues: Partial = getFormValues( ownProps.formName, )(state); const currentFiles = get(formValues, ownProps.configProperty, []); const pluginId = formValues.pluginId; const workpaceId = getCurrentWorkspaceId(state); return { pluginId, currentFiles, workpaceId }; }; export default connect(mapStateToProps)(MultipleFilePickerControl);