PromucFlow_constructor/app/client/src/widgets/FilepickerWidgetV2.tsx
Satish Gandham da1af9cac7
Filepicker v2 - Don't store large files in the dataStore (#6692)
* - Create a clone of the existing filepicker widget

* - If a file is larges than 5MB, don't put it in the dataTree.

Co-authored-by: Satish Gandham <satish@appsmith.com>
2021-08-24 09:08:57 +05:30

502 lines
15 KiB
TypeScript

import React from "react";
import BaseWidget, { WidgetProps, WidgetState } from "./BaseWidget";
import { WidgetType } from "constants/WidgetConstants";
import FilePickerComponent from "components/designSystems/appsmith/FilePickerComponent";
import Uppy from "@uppy/core";
import GoogleDrive from "@uppy/google-drive";
import Webcam from "@uppy/webcam";
import Url from "@uppy/url";
import OneDrive from "@uppy/onedrive";
import { ValidationTypes } from "constants/WidgetValidation";
import { EventType } from "constants/AppsmithActionConstants/ActionConstants";
import { DerivedPropertiesMap } from "utils/WidgetFactory";
import Dashboard from "@uppy/dashboard";
import shallowequal from "shallowequal";
import _, { findIndex } from "lodash";
import * as Sentry from "@sentry/react";
import withMeta, { WithMeta } from "./MetaHOC";
import FileDataTypes from "./FileDataTypes";
import { EvaluationSubstitutionType } from "entities/DataTree/dataTreeFactory";
import { createBlobUrl, isBlobUrl } from "utils/AppsmithUtils";
class FilePickerWidget extends BaseWidget<
FilePickerWidgetProps,
FilePickerWidgetState
> {
constructor(props: FilePickerWidgetProps) {
super(props);
this.state = {
isLoading: false,
uppy: this.initializeUppy(),
};
}
static getPropertyPaneConfig() {
return [
{
sectionName: "General",
children: [
{
propertyName: "label",
label: "Label",
controlType: "INPUT_TEXT",
helpText: "Sets the label of the button",
placeholderText: "Enter label text",
inputType: "TEXT",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.TEXT },
},
{
propertyName: "maxNumFiles",
label: "Max No. files",
helpText:
"Sets the maximum number of files that can be uploaded at once",
controlType: "INPUT_TEXT",
placeholderText: "Enter no. of files",
inputType: "INTEGER",
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.NUMBER },
},
{
propertyName: "maxFileSize",
helpText: "Sets the maximum size of each file that can be uploaded",
label: "Max file size(Mb)",
controlType: "INPUT_TEXT",
placeholderText: "File size in mb",
inputType: "INTEGER",
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.NUMBER,
params: { min: 1, max: 100, default: 5, required: true },
},
},
{
propertyName: "allowedFileTypes",
helpText: "Restricts the type of files which can be uploaded",
label: "Allowed File Types",
controlType: "MULTI_SELECT",
placeholderText: "Select file types",
options: [
{
label: "Any File",
value: "*",
},
{
label: "Images",
value: "image/*",
},
{
label: "Videos",
value: "video/*",
},
{
label: "Audio",
value: "audio/*",
},
{
label: "Text",
value: "text/*",
},
{
label: "MS Word",
value: ".doc",
},
{
label: "JPEG",
value: "image/jpeg",
},
{
label: "PNG",
value: ".png",
},
],
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: {
type: ValidationTypes.ARRAY,
params: {
allowedValues: [
"*",
"image/*",
"video/*",
"audio/*",
"text/*",
".doc",
"image/jpeg",
".png",
],
},
},
evaluationSubstitutionType:
EvaluationSubstitutionType.SMART_SUBSTITUTE,
},
{
helpText: "Set the format of the data read from the files",
propertyName: "fileDataType",
label: "Data Format",
controlType: "DROP_DOWN",
options: [
{
label: FileDataTypes.Base64,
value: FileDataTypes.Base64,
},
{
label: FileDataTypes.Binary,
value: FileDataTypes.Binary,
},
{
label: FileDataTypes.Text,
value: FileDataTypes.Text,
},
],
isBindProperty: false,
isTriggerProperty: false,
},
{
propertyName: "isRequired",
label: "Required",
helpText: "Makes input to the widget mandatory",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "isVisible",
label: "Visible",
helpText: "Controls the visibility of the widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
{
propertyName: "isDisabled",
label: "Disable",
helpText: "Disables input to this widget",
controlType: "SWITCH",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: false,
validation: { type: ValidationTypes.BOOLEAN },
},
],
},
{
sectionName: "Actions",
children: [
{
helpText:
"Triggers an action when the user selects a file. Upload files to a CDN and stores their URLs in filepicker.files",
propertyName: "onFilesSelected",
label: "onFilesSelected",
controlType: "ACTION_SELECTOR",
isJSConvertible: true,
isBindProperty: true,
isTriggerProperty: true,
},
],
},
];
}
static getDefaultPropertiesMap(): Record<string, string> {
return {
selectedFiles: "defaultSelectedFiles",
};
}
static getDerivedPropertiesMap(): DerivedPropertiesMap {
return {
isValid: `{{ this.isRequired ? this.files.length > 0 : true }}`,
files: `{{this.selectedFiles}}`,
};
}
static getMetaPropertiesMap(): Record<string, any> {
return {
selectedFiles: [],
uploadedFileData: {},
};
}
/**
* if uppy is not initialized before, initialize it
* else setState of uppy instance
*/
initializeUppy = () => {
const uppyState = {
id: this.props.widgetId,
autoProceed: false,
allowMultipleUploads: true,
debug: false,
restrictions: {
maxFileSize: this.props.maxFileSize
? this.props.maxFileSize * 1024 * 1024
: null,
maxNumberOfFiles: this.props.maxNumFiles,
minNumberOfFiles: null,
allowedFileTypes:
this.props.allowedFileTypes &&
(this.props.allowedFileTypes.includes("*") ||
_.isEmpty(this.props.allowedFileTypes))
? null
: this.props.allowedFileTypes,
},
};
return Uppy(uppyState);
};
/**
* set states on the uppy instance with new values
*/
reinitializeUppy = (props: FilePickerWidgetProps) => {
const uppyState = {
id: props.widgetId,
autoProceed: false,
allowMultipleUploads: true,
debug: false,
restrictions: {
maxFileSize: props.maxFileSize ? props.maxFileSize * 1024 * 1024 : null,
maxNumberOfFiles: props.maxNumFiles,
minNumberOfFiles: null,
allowedFileTypes:
props.allowedFileTypes &&
(this.props.allowedFileTypes.includes("*") ||
_.isEmpty(props.allowedFileTypes))
? null
: props.allowedFileTypes,
},
};
this.state.uppy.setOptions(uppyState);
};
/**
* add all uppy events listeners needed
*/
initializeUppyEventListeners = () => {
this.state.uppy
.use(Dashboard, {
target: "body",
metaFields: [],
inline: false,
width: 750,
height: 550,
thumbnailWidth: 280,
showLinkToFileUploadResult: true,
showProgressDetails: false,
hideUploadButton: false,
hideProgressAfterFinish: false,
note: null,
closeAfterFinish: true,
closeModalOnClickOutside: true,
disableStatusBar: false,
disableInformer: false,
disableThumbnailGenerator: false,
disablePageScrollWhenModalOpen: true,
proudlyDisplayPoweredByUppy: false,
onRequestCloseModal: () => {
const plugin = this.state.uppy.getPlugin("Dashboard");
if (plugin) {
plugin.closeModal();
}
},
locale: {
strings: {
closeModal: "Close",
},
},
})
.use(GoogleDrive, { companionUrl: "https://companion.uppy.io" })
.use(Url, { companionUrl: "https://companion.uppy.io" })
.use(OneDrive, {
companionUrl: "https://companion.uppy.io/",
})
.use(Webcam, {
onBeforeSnapshot: () => Promise.resolve(),
countdown: false,
mirror: true,
facingMode: "user",
locale: {},
});
this.state.uppy.on("file-removed", (file: any) => {
const updatedFiles = this.props.selectedFiles
? this.props.selectedFiles.filter((dslFile) => {
return file.id !== dslFile.id;
})
: [];
this.props.updateWidgetMetaProperty("selectedFiles", updatedFiles);
});
this.state.uppy.on("files-added", (files: any[]) => {
const dslFiles = this.props.selectedFiles
? [...this.props.selectedFiles]
: [];
const fileCount = this.props.selectedFiles?.length || 0;
const fileReaderPromises = files.map((file, index) => {
return new Promise((resolve) => {
if (file.size < 5000 * 1000) {
const reader = new FileReader();
if (this.props.fileDataType === FileDataTypes.Base64) {
reader.readAsDataURL(file.data);
} else if (this.props.fileDataType === FileDataTypes.Binary) {
reader.readAsBinaryString(file.data);
} else {
reader.readAsText(file.data);
}
reader.onloadend = () => {
const newFile = {
type: file.type,
id: file.id,
data: reader.result,
name: file.meta ? file.meta.name : `File-${index + fileCount}`,
size: file.size,
};
resolve(newFile);
};
} else {
const data = createBlobUrl(file.data, this.props.fileDataType);
const newFile = {
type: file.type,
id: file.id,
data: data,
name: file.meta ? file.meta.name : `File-${index + fileCount}`,
size: file.size,
};
resolve(newFile);
}
});
});
Promise.all(fileReaderPromises).then((files) => {
this.props.updateWidgetMetaProperty(
"selectedFiles",
dslFiles.concat(files),
);
});
});
this.state.uppy.on("upload", () => {
this.onFilesSelected();
});
};
/**
* this function is called when user selects the files and it do two things:
* 1. calls the action if any
* 2. set isLoading prop to true when calling the action
*/
onFilesSelected = () => {
if (this.props.onFilesSelected) {
this.executeAction({
triggerPropertyName: "onFilesSelected",
dynamicString: this.props.onFilesSelected,
event: {
type: EventType.ON_FILES_SELECTED,
callback: this.handleActionComplete,
},
});
this.setState({ isLoading: true });
}
};
handleActionComplete = () => {
this.setState({ isLoading: false });
};
componentDidUpdate(prevProps: FilePickerWidgetProps) {
super.componentDidUpdate(prevProps);
if (
prevProps.selectedFiles &&
prevProps.selectedFiles.length > 0 &&
this.props.selectedFiles === undefined
) {
this.state.uppy.reset();
} else if (
!shallowequal(prevProps.allowedFileTypes, this.props.allowedFileTypes) ||
prevProps.maxNumFiles !== this.props.maxNumFiles ||
prevProps.maxFileSize !== this.props.maxFileSize
) {
this.reinitializeUppy(this.props);
}
this.clearFilesFromMemory(prevProps.selectedFiles);
}
// Reclaim the memory used by blobs.
clearFilesFromMemory(previousFiles: any[] = []) {
const { selectedFiles: newFiles = [] } = this.props;
previousFiles.forEach((file: any) => {
let { data: blobUrl } = file;
if (isBlobUrl(blobUrl)) {
if (findIndex(newFiles, (f) => f.data === blobUrl) === -1) {
blobUrl = blobUrl.split("?")[0];
URL.revokeObjectURL(blobUrl);
}
}
});
}
componentDidMount() {
super.componentDidMount();
this.initializeUppyEventListeners();
}
componentWillUnmount() {
this.state.uppy.close();
}
getPageView() {
return (
<FilePickerComponent
files={this.props.selectedFiles || []}
isDisabled={this.props.isDisabled}
isLoading={this.props.isLoading || this.state.isLoading}
key={this.props.widgetId}
label={this.props.label}
uppy={this.state.uppy}
widgetId={this.props.widgetId}
/>
);
}
getWidgetType(): WidgetType {
return "FILE_PICKER_WIDGET";
}
}
interface FilePickerWidgetState extends WidgetState {
isLoading: boolean;
uppy: any;
}
interface FilePickerWidgetProps extends WidgetProps, WithMeta {
label: string;
maxNumFiles?: number;
maxFileSize?: number;
selectedFiles?: any[];
allowedFileTypes: string[];
onFilesSelected?: string;
fileDataType: FileDataTypes;
isRequired?: boolean;
}
export type FilePickerWidgetV2Props = FilePickerWidgetProps;
export type FilePickerWidgetV2State = FilePickerWidgetState;
export default FilePickerWidget;
export const ProfiledFilePickerWidgetV2 = Sentry.withProfiler(
withMeta(FilePickerWidget),
);