mirror of
https://github.com/status-im/spiff-arena.git
synced 2025-03-01 01:40:42 +00:00
rjsf-fileupload-error-indicator (#1661)
* added formatting to filewidget when it gives an error w/ burnettk * use the mui icon button for the file remove rjsf button w/ burnettk --------- Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
parent
1f6ca8b727
commit
b301af190c
@ -0,0 +1,311 @@
|
|||||||
|
import { ChangeEvent, useCallback, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
dataURItoBlob,
|
||||||
|
FormContextType,
|
||||||
|
getTemplate,
|
||||||
|
Registry,
|
||||||
|
RJSFSchema,
|
||||||
|
StrictRJSFSchema,
|
||||||
|
TranslatableString,
|
||||||
|
UIOptionsType,
|
||||||
|
WidgetProps,
|
||||||
|
} from '@rjsf/utils';
|
||||||
|
import Markdown from 'markdown-to-jsx';
|
||||||
|
import { FileUploader } from '@carbon/react';
|
||||||
|
import { getCommonAttributes } from '../../helpers';
|
||||||
|
|
||||||
|
function addNameToDataURL(dataURL: string, name: string) {
|
||||||
|
if (dataURL === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return dataURL.replace(';base64', `;name=${encodeURIComponent(name)};base64`);
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileInfoType = {
|
||||||
|
dataURL?: string | null;
|
||||||
|
name: string;
|
||||||
|
size: number;
|
||||||
|
type: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function processFile(file: File): Promise<FileInfoType> {
|
||||||
|
const { name, size, type } = file;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new window.FileReader();
|
||||||
|
reader.onerror = reject;
|
||||||
|
reader.onload = (event) => {
|
||||||
|
if (typeof event.target?.result === 'string') {
|
||||||
|
resolve({
|
||||||
|
dataURL: addNameToDataURL(event.target.result, name),
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
dataURL: null,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processFiles(files: FileList) {
|
||||||
|
return Promise.all(Array.from(files).map(processFile));
|
||||||
|
}
|
||||||
|
|
||||||
|
function FileInfoPreview<
|
||||||
|
T = any,
|
||||||
|
S extends StrictRJSFSchema = RJSFSchema,
|
||||||
|
F extends FormContextType = any
|
||||||
|
>({
|
||||||
|
fileInfo,
|
||||||
|
registry,
|
||||||
|
}: {
|
||||||
|
fileInfo: FileInfoType;
|
||||||
|
registry: Registry<T, S, F>;
|
||||||
|
}) {
|
||||||
|
const { translateString } = registry;
|
||||||
|
const { dataURL, type, name } = fileInfo;
|
||||||
|
if (!dataURL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If type is JPEG or PNG then show image preview.
|
||||||
|
// Originally, any type of image was supported, but this was changed into a whitelist
|
||||||
|
// since SVGs and animated GIFs are also images, which are generally considered a security risk.
|
||||||
|
if (['image/jpeg', 'image/png'].includes(type)) {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
src={dataURL}
|
||||||
|
style={{ maxWidth: '100%' }}
|
||||||
|
className="file-preview"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, let users download file
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{' '}
|
||||||
|
<a download={`preview-${name}`} href={dataURL} className="file-download">
|
||||||
|
{translateString(TranslatableString.PreviewLabel)}
|
||||||
|
</a>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FilesInfo<
|
||||||
|
T = any,
|
||||||
|
S extends StrictRJSFSchema = RJSFSchema,
|
||||||
|
F extends FormContextType = any
|
||||||
|
>({
|
||||||
|
filesInfo,
|
||||||
|
registry,
|
||||||
|
preview,
|
||||||
|
onRemove,
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
filesInfo: FileInfoType[];
|
||||||
|
registry: Registry<T, S, F>;
|
||||||
|
preview?: boolean;
|
||||||
|
onRemove: (index: number) => void;
|
||||||
|
options: UIOptionsType<T, S, F>;
|
||||||
|
}) {
|
||||||
|
if (filesInfo.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const { translateString } = registry;
|
||||||
|
|
||||||
|
// MuiRemoveButton doesn't exist on the correct component type but we add that manually so we know it is there
|
||||||
|
const { MuiRemoveButton } = getTemplate<any, any, any>(
|
||||||
|
'ButtonTemplates',
|
||||||
|
registry,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="file-info">
|
||||||
|
{filesInfo.map((fileInfo, key) => {
|
||||||
|
const { name, size, type } = fileInfo;
|
||||||
|
const handleRemove = () => onRemove(key);
|
||||||
|
return (
|
||||||
|
<li key={key}>
|
||||||
|
<Markdown>
|
||||||
|
{translateString(TranslatableString.FilesInfo, [
|
||||||
|
name,
|
||||||
|
type,
|
||||||
|
String(size),
|
||||||
|
])}
|
||||||
|
</Markdown>
|
||||||
|
{preview && (
|
||||||
|
<FileInfoPreview<T, S, F>
|
||||||
|
fileInfo={fileInfo}
|
||||||
|
registry={registry}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<MuiRemoveButton onClick={handleRemove} registry={registry} />
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFileInfo(dataURLs: string[]): FileInfoType[] {
|
||||||
|
return dataURLs.reduce((acc, dataURL) => {
|
||||||
|
if (!dataURL) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const { blob, name } = dataURItoBlob(dataURL);
|
||||||
|
return [
|
||||||
|
...acc,
|
||||||
|
{
|
||||||
|
dataURL,
|
||||||
|
name: name,
|
||||||
|
size: blob.size,
|
||||||
|
type: blob.type,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
} catch (e) {
|
||||||
|
// Invalid dataURI, so just ignore it.
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
}, [] as FileInfoType[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The `FileWidget` is a widget for rendering file upload fields.
|
||||||
|
* It is typically used with a string property with data-url format.
|
||||||
|
*/
|
||||||
|
function FileWidget<
|
||||||
|
T = any,
|
||||||
|
S extends StrictRJSFSchema = RJSFSchema,
|
||||||
|
F extends FormContextType = any
|
||||||
|
>(props: WidgetProps<T, S, F>) {
|
||||||
|
const {
|
||||||
|
disabled,
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
multiple,
|
||||||
|
onChange,
|
||||||
|
options,
|
||||||
|
rawErrors,
|
||||||
|
readonly,
|
||||||
|
registry,
|
||||||
|
required,
|
||||||
|
schema,
|
||||||
|
uiSchema,
|
||||||
|
value,
|
||||||
|
} = props;
|
||||||
|
const BaseInputTemplate = getTemplate<'BaseInputTemplate', T, S, F>(
|
||||||
|
'BaseInputTemplate',
|
||||||
|
registry,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleChange = useCallback(
|
||||||
|
(event: ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!event.target.files) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Due to variances in themes, dealing with multiple files for the array case now happens one file at a time.
|
||||||
|
// This is because we don't pass `multiple` into the `BaseInputTemplate` anymore. Instead, we deal with the single
|
||||||
|
// file in each event and concatenate them together ourselves
|
||||||
|
processFiles(event.target.files).then((filesInfoEvent) => {
|
||||||
|
const newValue = filesInfoEvent.map((fileInfo) => fileInfo.dataURL);
|
||||||
|
if (multiple) {
|
||||||
|
onChange(value.concat(newValue[0]));
|
||||||
|
} else {
|
||||||
|
onChange(newValue[0]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[multiple, value, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
const commonAttributes = getCommonAttributes(
|
||||||
|
label,
|
||||||
|
schema,
|
||||||
|
uiSchema,
|
||||||
|
rawErrors
|
||||||
|
);
|
||||||
|
|
||||||
|
const filesInfo = useMemo(
|
||||||
|
() => extractFileInfo(Array.isArray(value) ? value : [value]),
|
||||||
|
[value]
|
||||||
|
);
|
||||||
|
const rmFile = useCallback(
|
||||||
|
(index: number) => {
|
||||||
|
if (multiple) {
|
||||||
|
const newValue = value.filter((_: any, i: number) => i !== index);
|
||||||
|
onChange(newValue);
|
||||||
|
} else {
|
||||||
|
onChange(undefined);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[multiple, value, onChange]
|
||||||
|
);
|
||||||
|
|
||||||
|
let wrapperProps = null;
|
||||||
|
let errorSvg = null;
|
||||||
|
if (commonAttributes.invalid) {
|
||||||
|
wrapperProps = { 'data-invalid': true };
|
||||||
|
errorSvg = (
|
||||||
|
<svg
|
||||||
|
focusable="false"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
fill="currentColor"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 16 16"
|
||||||
|
aria-hidden="true"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="cds--text-input__invalid-icon"
|
||||||
|
>
|
||||||
|
<path d="M8,1C4.2,1,1,4.2,1,8s3.2,7,7,7s7-3.1,7-7S11.9,1,8,1z M7.5,4h1v5h-1C7.5,9,7.5,4,7.5,4z M8,12.2 c-0.4,0-0.8-0.4-0.8-0.8s0.3-0.8,0.8-0.8c0.4,0,0.8,0.4,0.8,0.8S8.4,12.2,8,12.2z"></path>
|
||||||
|
<path
|
||||||
|
d="M7.5,4h1v5h-1C7.5,9,7.5,4,7.5,4z M8,12.2c-0.4,0-0.8-0.4-0.8-0.8s0.3-0.8,0.8-0.8 c0.4,0,0.8,0.4,0.8,0.8S8.4,12.2,8,12.2z"
|
||||||
|
data-icon-path="inner-path"
|
||||||
|
opacity="0"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="cds--text-input__field-wrapper" {...wrapperProps}>
|
||||||
|
{errorSvg}
|
||||||
|
<BaseInputTemplate
|
||||||
|
{...props}
|
||||||
|
disabled={disabled || readonly}
|
||||||
|
type="file"
|
||||||
|
required={value ? false : required} // this turns off HTML required validation when a value exists
|
||||||
|
onChangeOverride={handleChange}
|
||||||
|
value=""
|
||||||
|
accept={options.accept ? String(options.accept) : undefined}
|
||||||
|
/>
|
||||||
|
<span role="alert" className="cds--text-input__counter-alert"></span>
|
||||||
|
<FilesInfo<T, S, F>
|
||||||
|
filesInfo={filesInfo}
|
||||||
|
onRemove={rmFile}
|
||||||
|
registry={registry}
|
||||||
|
preview={options.filePreview}
|
||||||
|
options={options}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id={`${id}-error-msg`} className="cds--form-requirement">
|
||||||
|
{commonAttributes.errorMessageForField}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileWidget;
|
@ -0,0 +1,2 @@
|
|||||||
|
export { default } from './FileWidget';
|
||||||
|
export * from './FileWidget';
|
@ -7,6 +7,9 @@ import {
|
|||||||
StrictRJSFSchema,
|
StrictRJSFSchema,
|
||||||
} from '@rjsf/utils';
|
} from '@rjsf/utils';
|
||||||
|
|
||||||
|
import RemoveIcon from '@mui/icons-material/Remove';
|
||||||
|
import { IconButton as MuiIconButton } from '@mui/material';
|
||||||
|
|
||||||
import { Add, TrashCan, ArrowUp, ArrowDown } from '@carbon/icons-react';
|
import { Add, TrashCan, ArrowUp, ArrowDown } from '@carbon/icons-react';
|
||||||
|
|
||||||
export default function IconButton<
|
export default function IconButton<
|
||||||
@ -34,6 +37,9 @@ export default function IconButton<
|
|||||||
if (icon === 'arrow-down') {
|
if (icon === 'arrow-down') {
|
||||||
carbonIcon = ArrowDown;
|
carbonIcon = ArrowDown;
|
||||||
}
|
}
|
||||||
|
if (icon === 'core-remove') {
|
||||||
|
carbonIcon = RemoveIcon;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
@ -94,3 +100,20 @@ export function RemoveButton<
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MuiRemoveButton<
|
||||||
|
T = any,
|
||||||
|
S extends StrictRJSFSchema = RJSFSchema,
|
||||||
|
F extends FormContextType = any
|
||||||
|
>(props: IconButtonProps<T, S, F>) {
|
||||||
|
return (
|
||||||
|
<MuiIconButton
|
||||||
|
title="Remove"
|
||||||
|
{...props}
|
||||||
|
color="warning"
|
||||||
|
aria-label="Remove"
|
||||||
|
>
|
||||||
|
<RemoveIcon />
|
||||||
|
</MuiIconButton>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -4,7 +4,12 @@ import ArrayFieldTemplate from '../ArrayFieldTemplate';
|
|||||||
import BaseInputTemplate from '../BaseInputTemplate';
|
import BaseInputTemplate from '../BaseInputTemplate';
|
||||||
import DescriptionField from '../DescriptionField';
|
import DescriptionField from '../DescriptionField';
|
||||||
import ErrorList from '../ErrorList';
|
import ErrorList from '../ErrorList';
|
||||||
import { MoveDownButton, MoveUpButton, RemoveButton } from '../IconButton';
|
import {
|
||||||
|
MuiRemoveButton,
|
||||||
|
MoveDownButton,
|
||||||
|
MoveUpButton,
|
||||||
|
RemoveButton,
|
||||||
|
} from '../IconButton';
|
||||||
import FieldErrorTemplate from '../FieldErrorTemplate';
|
import FieldErrorTemplate from '../FieldErrorTemplate';
|
||||||
import FieldHelpTemplate from '../FieldHelpTemplate';
|
import FieldHelpTemplate from '../FieldHelpTemplate';
|
||||||
import FieldTemplate from '../FieldTemplate';
|
import FieldTemplate from '../FieldTemplate';
|
||||||
@ -19,6 +24,7 @@ export default {
|
|||||||
BaseInputTemplate,
|
BaseInputTemplate,
|
||||||
ButtonTemplates: {
|
ButtonTemplates: {
|
||||||
AddButton,
|
AddButton,
|
||||||
|
MuiRemoveButton,
|
||||||
MoveDownButton,
|
MoveDownButton,
|
||||||
MoveUpButton,
|
MoveUpButton,
|
||||||
RemoveButton,
|
RemoveButton,
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import CheckboxWidget from '../CheckboxWidget/CheckboxWidget';
|
import CheckboxWidget from '../CheckboxWidget/CheckboxWidget';
|
||||||
import CheckboxesWidget from '../CheckboxesWidget/CheckboxesWidget';
|
import CheckboxesWidget from '../CheckboxesWidget/CheckboxesWidget';
|
||||||
import DateWidget from '../DateWidget/DateWidget';
|
|
||||||
import DateTimeWidget from '../DateTimeWidget/DateTimeWidget';
|
import DateTimeWidget from '../DateTimeWidget/DateTimeWidget';
|
||||||
|
import DateWidget from '../DateWidget/DateWidget';
|
||||||
|
import FileWidget from '../FileWidget/FileWidget';
|
||||||
import RadioWidget from '../RadioWidget/RadioWidget';
|
import RadioWidget from '../RadioWidget/RadioWidget';
|
||||||
import RangeWidget from '../RangeWidget/RangeWidget';
|
import RangeWidget from '../RangeWidget/RangeWidget';
|
||||||
import SelectWidget from '../SelectWidget/SelectWidget';
|
import SelectWidget from '../SelectWidget/SelectWidget';
|
||||||
@ -10,8 +11,9 @@ import TextareaWidget from '../TextareaWidget/TextareaWidget';
|
|||||||
export default {
|
export default {
|
||||||
CheckboxWidget,
|
CheckboxWidget,
|
||||||
CheckboxesWidget,
|
CheckboxesWidget,
|
||||||
DateWidget,
|
|
||||||
DateTimeWidget,
|
DateTimeWidget,
|
||||||
|
DateWidget,
|
||||||
|
FileWidget,
|
||||||
RadioWidget,
|
RadioWidget,
|
||||||
RangeWidget,
|
RangeWidget,
|
||||||
SelectWidget,
|
SelectWidget,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user