diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx index 79365357f41..6524f3e07e4 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/BindableEditor.tsx @@ -7,6 +7,7 @@ import { getDefaultControl } from '../../propertyControls'; function renderDefaultControl(params: RenderControlParams) { const Control = getDefaultControl({ typeDef: params.propType }); + return Control ? : null; } diff --git a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx index 344e6447203..5ca1efa45e6 100644 --- a/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx +++ b/packages/toolpad-app/src/toolpad/AppEditor/PageEditor/ComponentCatalog/ComponentCatalogItem.tsx @@ -17,7 +17,9 @@ import DateRangeIcon from '@mui/icons-material/DateRange'; import CheckBoxIcon from '@mui/icons-material/CheckBox'; import DashboardCustomizeSharpIcon from '@mui/icons-material/DashboardCustomizeSharp'; import AddIcon from '@mui/icons-material/Add'; +import UploadFileIcon from '@mui/icons-material/UploadFile'; import NotesIcon from '@mui/icons-material/Notes'; + import { SvgIconProps } from '@mui/material/SvgIcon'; const iconMap = new Map>([ @@ -35,6 +37,7 @@ const iconMap = new Map>([ ['Switch', ToggleOnIcon], ['Radio', RadioButtonCheckedIcon], ['DatePicker', DateRangeIcon], + ['FilePicker', UploadFileIcon], ['Checkbox', CheckBoxIcon], ['CodeComponent', DashboardCustomizeSharpIcon], ['CreateNew', AddIcon], diff --git a/packages/toolpad-app/src/toolpad/propertyControls/file.tsx b/packages/toolpad-app/src/toolpad/propertyControls/file.tsx new file mode 100644 index 00000000000..a6e91780c83 --- /dev/null +++ b/packages/toolpad-app/src/toolpad/propertyControls/file.tsx @@ -0,0 +1,31 @@ +import { Typography } from '@mui/material'; +import * as React from 'react'; +import type { EditorProps } from '../../types'; + +function FilePropEditor({ value = [] }: EditorProps) { + const files = Array.from(value); + const hasSelectedFiles = files?.length > 0; + + if (!hasSelectedFiles) { + return ( + + No files chosen + + ); + } + + return ( +
+ + Files: + + {files.map(({ name }) => ( + + {name} + + ))} +
+ ); +} + +export default FilePropEditor; diff --git a/packages/toolpad-app/src/toolpad/propertyControls/index.tsx b/packages/toolpad-app/src/toolpad/propertyControls/index.tsx index c22ce5ee44c..f63e485357f 100644 --- a/packages/toolpad-app/src/toolpad/propertyControls/index.tsx +++ b/packages/toolpad-app/src/toolpad/propertyControls/index.tsx @@ -11,6 +11,7 @@ import SelectOptions from './SelectOptions'; import HorizontalAlign from './HorizontalAlign'; import VerticalAlign from './VerticalAlign'; import RowIdFieldSelect from './RowIdFieldSelect'; +import file from './file'; import { EditorProps } from '../../types'; const propTypeControls: { @@ -28,6 +29,7 @@ const propTypeControls: { RowIdFieldSelect, HorizontalAlign, VerticalAlign, + file, }; function getDefaultControlForType(propType: PropValueType): React.FC> | null { @@ -44,6 +46,8 @@ function getDefaultControlForType(propType: PropValueType): React.FC([ ['DataGrid', { displayName: 'Data grid', builtIn: 'DataGrid' }], ['TextField', { displayName: 'Text field', builtIn: 'TextField' }], ['DatePicker', { displayName: 'Date picker', builtIn: 'DatePicker' }], + ['FilePicker', { displayName: 'File picker', builtIn: 'FilePicker' }], ['Text', { displayName: 'Text', builtIn: 'Text' }], ['Select', { displayName: 'Select', builtIn: 'Select' }], ['Paper', { displayName: 'Paper', builtIn: 'Paper' }], diff --git a/packages/toolpad-components/src/FilePicker.tsx b/packages/toolpad-components/src/FilePicker.tsx new file mode 100644 index 00000000000..748373f9f9a --- /dev/null +++ b/packages/toolpad-components/src/FilePicker.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import { TextField as MuiTextField, TextFieldProps as MuiTextFieldProps } from '@mui/material'; +import { createComponent } from '@mui/toolpad-core'; + +interface FullFile { + name: string; + type: string; + size: number; + base64: null | string; +} + +export type Props = MuiTextFieldProps & { + multiple: boolean; + onChange: (files: FullFile[]) => void; +}; + +const readFile = async (file: Blob): Promise => { + return new Promise((resolve, reject) => { + const readerBase64 = new FileReader(); + + readerBase64.onload = (event) => { + if (!event.target) { + reject(); + return; + } + + resolve(event.target.result as string); + }; + + readerBase64.readAsDataURL(file); + }); +}; + +function FilePicker({ multiple, onChange, ...props }: Props) { + const handleChange = async (changeEvent: React.ChangeEvent) => { + const filesPromises = Array.from(changeEvent.target.files || []).map(async (file) => { + const fullFile: FullFile = { + name: file.name, + type: file.type, + size: file.size, + base64: await readFile(file), + }; + + return fullFile; + }); + + const files = await Promise.all(filesPromises); + + onChange(files); + }; + + return ( + + ); +} + +export default createComponent(FilePicker, { + argTypes: { + value: { + typeDef: { type: 'file' }, + onChangeProp: 'onChange', + }, + label: { + typeDef: { type: 'string' }, + }, + variant: { + typeDef: { type: 'string', enum: ['outlined', 'filled', 'standard'] }, + defaultValue: 'outlined', + }, + size: { + typeDef: { type: 'string', enum: ['small', 'normal'] }, + defaultValue: 'small', + }, + multiple: { + typeDef: { type: 'boolean' }, + defaultValue: true, + }, + fullWidth: { + typeDef: { type: 'boolean' }, + }, + disabled: { + typeDef: { type: 'boolean' }, + }, + sx: { + typeDef: { type: 'object' }, + }, + }, +}); diff --git a/packages/toolpad-components/src/index.tsx b/packages/toolpad-components/src/index.tsx index b9577f742f4..99e4e8fd4c4 100644 --- a/packages/toolpad-components/src/index.tsx +++ b/packages/toolpad-components/src/index.tsx @@ -20,5 +20,7 @@ export { default as Image } from './Image.js'; export { default as DatePicker } from './DatePicker.js'; +export { default as FilePicker } from './FilePicker.js'; + export { CUSTOM_COLUMN_TYPES, NUMBER_FORMAT_PRESETS, inferColumns, parseColumns } from './DataGrid'; export type { SerializableGridColumn, SerializableGridColumns, NumberFormat } from './DataGrid'; diff --git a/packages/toolpad-core/src/types.ts b/packages/toolpad-core/src/types.ts index db9d5a1a401..d14bdc80726 100644 --- a/packages/toolpad-core/src/types.ts +++ b/packages/toolpad-core/src/types.ts @@ -75,7 +75,7 @@ export type BindableAttrEntries = [string, BindableAttrValue][]; export type SlotType = 'single' | 'multiple' | 'layout'; export interface ValueTypeBase { - type: 'string' | 'boolean' | 'number' | 'object' | 'array' | 'element' | 'event'; + type: 'string' | 'boolean' | 'number' | 'object' | 'array' | 'element' | 'event' | 'file'; } export interface StringValueType extends ValueTypeBase { @@ -103,6 +103,10 @@ export interface ArrayValueType extends ValueTypeBase { schema?: string; } +export interface FileValueType extends ValueTypeBase { + type: 'file'; +} + export interface ElementValueType extends ValueTypeBase { type: 'element'; } @@ -138,7 +142,8 @@ export interface ArgControlSpec { | 'HorizontalAlign' | 'VerticalAlign' | 'event' - | 'RowIdFieldSelect'; // Row id field specialized select + | 'RowIdFieldSelect' // Row id field specialized select + | 'file'; } type PrimitiveValueType = @@ -146,7 +151,8 @@ type PrimitiveValueType = | NumberValueType | BooleanValueType | ObjectValueType - | ArrayValueType; + | ArrayValueType + | FileValueType; export type PropValueType = PrimitiveValueType | ElementValueType | EventValueType;