PromucFlow_constructor/app/client/src/components/formControls/MultiFilePickerControl.tsx
Diljit 007e70a757
fix: Remove stale values in Query form on view change (#30975)
## Description
- Add a Mixpanel event for Max limit validation for Multi file picker
- Fix the bug where stale values of the Query pane form gets populated
in the Query editor when changing views.

Fixes # (issue number)
https://github.com/appsmithorg/appsmith-ee/issues/3499
2024-02-09 09:36:45 +05:30

321 lines
8.1 KiB
TypeScript

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 type { Plugin } from "api/PluginApi";
import { get, isArray } from "lodash";
import { formatFileSize } from "./utils";
import { getCurrentWorkspaceId } from "@appsmith/selectors/selectedWorkspaceSelectors";
import { getPlugin } from "@appsmith/selectors/entitiesSelector";
import AnalyticsUtil from "utils/AnalyticsUtil";
const HiddenFileInput = styled.input`
visibility: hidden;
`;
interface ConnectProps {
pluginId?: string;
currentFiles: FileMetadata[];
workpaceId: string;
plugin?: Plugin;
}
export type MultipleFilePickerControlProps = ControlProps & {
allowedFileTypes?: string[];
maxFileSizeInBytes?: number;
uploadFilesToTrigger?: boolean;
pluginId?: string;
config?: {
uploadToTrigger?: boolean;
params?: Record<string, any>;
};
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<FileMetadata[]>([]);
const fileInputRef = React.useRef<HTMLInputElement>(null);
const uploadFilesToTriggerApi = async (
files: File[],
): Promise<FileMetadata[]> => {
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",
},
);
AnalyticsUtil.logEvent("MULTI_FILE_PICKER_EXCEEDS_LIMIT", {
uploadedSize: totalSize,
allowedSize: maxFileSizeInBytes,
pluginName: props.plugin?.name,
pluginId: props.pluginId,
packageName: props.plugin?.packageName,
});
clearInput();
return false;
}
return true;
};
const clearInput = () => {
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};
const getBase64Content = async (file: File): Promise<string> => {
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<HTMLInputElement> = 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 (
<div>
<div className="flex items-center gap-2">
<Button
isDisabled={uploading}
isLoading={uploading}
kind="secondary"
onClick={() => {
if (fileInputRef.current) {
fileInputRef.current.click();
}
}}
>
{buttonLabel}
</Button>
<HiddenFileInput
accept={allowedFileTypesProps}
multiple
onChange={handleFileChange}
ref={fileInputRef}
type="file"
/>
</div>
<div className="flex gap-2 flex-col mt-2">
{uploadedFiles.map((file) => (
<Tag
className="w-max !border-[color:var(--ads-v2\-color-gray-400)]"
key={file.id}
onClose={() => {
onRemoveFile(file.id);
}}
size="md"
>
<div className="flex items-center">
<Text
className="min-w-[120px] max-w-[120px] truncate inline-block mr-2"
kind="body-s"
>
{file.name}
</Text>
<Text
className="min-w-[90px] max-w-[90px] inline-block text-right"
kind="body-s"
>
({formatFileSize(file.size)})
</Text>
</div>
</Tag>
))}
</div>
</div>
);
}
class MultipleFilePickerControl extends BaseControl<MultipleFilePickerControlProps> {
constructor(props: MultipleFilePickerControlProps) {
super(props);
this.state = {
isOpen: false,
};
}
render() {
const { configProperty, disabled } = this.props;
return (
<Field
component={FilePicker}
disabled={disabled}
name={configProperty}
props={this.props}
/>
);
}
getControlType(): ControlType {
return "MULTIPLE_FILE_PICKER";
}
}
export interface FilePickerComponentState {
isOpen: boolean;
}
const mapStateToProps = (
state: AppState,
ownProps: MultipleFilePickerControlProps,
): ConnectProps => {
const formValues: Partial<Action | Datasource> = getFormValues(
ownProps.formName,
)(state);
const currentFiles = get(formValues, ownProps.configProperty, []);
const pluginId = formValues.pluginId || "";
const workpaceId = getCurrentWorkspaceId(state);
const plugin = getPlugin(state, pluginId);
return { plugin, pluginId, currentFiles, workpaceId };
};
export default connect(mapStateToProps)(MultipleFilePickerControl);