Skip to content

Commit deb698c

Browse files
committed
fix: intercept react-butterfiles and use a patched implementation
1 parent 60e8e8f commit deb698c

File tree

7 files changed

+297
-0
lines changed

7 files changed

+297
-0
lines changed

packages/app/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@
3333
"apollo-link-http-common": "^0.2.16",
3434
"apollo-utilities": "^1.3.4",
3535
"boolean": "^3.0.1",
36+
"bytes": "^3.0.0",
3637
"graphql": "^15.7.2",
3738
"invariant": "^2.2.4",
3839
"lodash": "^4.17.21",
40+
"minimatch": "^5.1.0",
3941
"nanoid": "^3.3.7",
4042
"react": "18.2.0",
4143
"react-dom": "18.2.0",
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import React from "react";
2+
import bytes from "bytes";
3+
import minimatch from "minimatch";
4+
import { readFileContent } from "./utils/readFileContent";
5+
import { generateId } from "./utils/generateId";
6+
7+
export type SelectedFile = {
8+
id: string;
9+
name: string;
10+
type: string;
11+
size: number;
12+
src: {
13+
file: File;
14+
base64: string | null;
15+
};
16+
};
17+
18+
export type FileError = {
19+
id: string;
20+
type:
21+
| "unsupportedFileType"
22+
| "maxSizeExceeded"
23+
| "multipleMaxSizeExceeded"
24+
| "multipleMaxCountExceeded"
25+
| "multipleNotAllowed";
26+
index?: number;
27+
file?: SelectedFile | File;
28+
multipleFileSize?: number;
29+
multipleMaxSize?: number;
30+
multipleMaxCount?: number;
31+
multipleCount?: number;
32+
};
33+
34+
export type BrowseFilesParams = {
35+
onSuccess?: (files: SelectedFile[]) => void;
36+
onError?: (errors: FileError[], files: SelectedFile[]) => void;
37+
};
38+
39+
export type RenderPropParams = {
40+
browseFiles: (params: BrowseFilesParams) => void;
41+
getDropZoneProps: (additionalProps: any) => any;
42+
getLabelProps: (additionalProps: any) => any;
43+
validateFiles: (files: SelectedFile[] | File[]) => FileError[];
44+
};
45+
46+
export type FilesRules = {
47+
accept: string[];
48+
multiple: boolean;
49+
maxSize: string;
50+
multipleMaxSize: string;
51+
multipleMaxCount: number | null;
52+
convertToBase64: boolean;
53+
onSuccess?: (files: SelectedFile[]) => void;
54+
onError?: (errors: FileError[], files: SelectedFile[]) => void;
55+
};
56+
57+
export type Props = FilesRules & {
58+
children: (params: RenderPropParams) => React.ReactNode;
59+
id?: string;
60+
};
61+
62+
export class Files extends React.Component<Props> {
63+
static defaultProps = {
64+
accept: [],
65+
multiple: false,
66+
maxSize: "2mb",
67+
multipleMaxSize: "10mb",
68+
multipleMaxCount: null,
69+
convertToBase64: false
70+
};
71+
72+
input: HTMLInputElement | null = null;
73+
browseFilesPassedParams: BrowseFilesParams | null = null;
74+
id: string = generateId();
75+
76+
validateFiles = (files: SelectedFile[] | File[]): FileError[] => {
77+
const { multiple, multipleMaxSize, multipleMaxCount, accept, maxSize } = this.props;
78+
79+
const errors: FileError[] = [];
80+
let multipleFileSize = 0;
81+
82+
if (!multiple && files.length > 1) {
83+
errors.push({
84+
id: generateId(),
85+
type: "multipleNotAllowed"
86+
});
87+
88+
return errors;
89+
}
90+
91+
for (let index = 0; index < files.length; index++) {
92+
const file = files[index];
93+
94+
if (
95+
Array.isArray(accept) &&
96+
accept.length &&
97+
!accept.some(type => minimatch(file.type, type))
98+
) {
99+
errors.push({
100+
id: generateId(),
101+
index,
102+
file,
103+
type: "unsupportedFileType"
104+
});
105+
} else if (maxSize) {
106+
if (file.size > bytes(maxSize)) {
107+
errors.push({
108+
id: generateId(),
109+
index,
110+
file,
111+
type: "maxSizeExceeded"
112+
});
113+
}
114+
}
115+
116+
if (multiple) {
117+
multipleFileSize += file.size;
118+
}
119+
}
120+
121+
if (multiple) {
122+
if (multipleMaxSize && multipleFileSize > bytes(multipleMaxSize)) {
123+
errors.push({
124+
id: generateId(),
125+
type: "multipleMaxSizeExceeded",
126+
multipleFileSize,
127+
multipleMaxSize: bytes(multipleMaxSize)
128+
});
129+
}
130+
131+
if (multipleMaxCount && files.length > multipleMaxCount) {
132+
errors.push({
133+
id: generateId(),
134+
type: "multipleMaxCountExceeded",
135+
multipleCount: files.length,
136+
multipleMaxCount
137+
});
138+
}
139+
}
140+
141+
return errors;
142+
};
143+
144+
processSelectedFiles = async (eventFiles: Array<File>) => {
145+
if (eventFiles.length === 0) {
146+
return;
147+
}
148+
149+
const { convertToBase64, onSuccess, onError } = this.props;
150+
const { browseFilesPassedParams } = this;
151+
const callbacks = {
152+
onSuccess,
153+
onError
154+
};
155+
156+
if (browseFilesPassedParams && browseFilesPassedParams.onSuccess) {
157+
callbacks.onSuccess = browseFilesPassedParams.onSuccess;
158+
}
159+
160+
if (browseFilesPassedParams && browseFilesPassedParams.onError) {
161+
callbacks.onError = browseFilesPassedParams.onError;
162+
}
163+
164+
const files: SelectedFile[] = [...eventFiles].map(file => {
165+
return {
166+
id: generateId(),
167+
name: file.name,
168+
type: file.type,
169+
size: file.size,
170+
src: {
171+
file,
172+
base64: null
173+
}
174+
};
175+
});
176+
177+
const errors = this.validateFiles(files);
178+
179+
if (errors.length) {
180+
callbacks.onError && callbacks.onError(errors, files);
181+
} else {
182+
if (convertToBase64) {
183+
for (let i = 0; i < files.length; i++) {
184+
const file = files[i].src.file;
185+
files[i].src.base64 = await readFileContent(file);
186+
}
187+
}
188+
189+
callbacks.onSuccess && callbacks.onSuccess(files);
190+
}
191+
192+
// Reset the browseFiles arguments.
193+
if (this.input) {
194+
this.input.value = "";
195+
}
196+
this.browseFilesPassedParams = null;
197+
};
198+
199+
/**
200+
* Extracted into a separate method just for testing purposes.
201+
*/
202+
onDropFilesHandler = async ({ e, onSuccess, onError }: any) => {
203+
this.browseFilesPassedParams = { onSuccess, onError };
204+
e.dataTransfer &&
205+
e.dataTransfer.files &&
206+
(await this.processSelectedFiles(e.dataTransfer.files));
207+
};
208+
209+
/**
210+
* Extracted into a separate method just for testing purposes.
211+
*/
212+
browseFilesHandler = ({ onSuccess, onError }: any) => {
213+
this.browseFilesPassedParams = { onSuccess, onError };
214+
this.input && this.input.click();
215+
};
216+
217+
override render() {
218+
const { multiple, accept, id } = this.props;
219+
return (
220+
<React.Fragment>
221+
{this.props.children({
222+
getLabelProps: (props: any) => {
223+
return {
224+
...props,
225+
htmlFor: id || this.id
226+
};
227+
},
228+
validateFiles: this.validateFiles,
229+
browseFiles: ({ onSuccess, onError }: BrowseFilesParams = {}) => {
230+
this.browseFilesHandler({ onSuccess, onError });
231+
},
232+
getDropZoneProps: ({
233+
onSuccess,
234+
onError,
235+
onDragOver,
236+
onDrop,
237+
...rest
238+
}: any = {}) => {
239+
return {
240+
...rest,
241+
onDragOver: (e: DragEvent) => {
242+
e.preventDefault();
243+
typeof onDragOver === "function" && onDragOver();
244+
},
245+
onDrop: async (e: DragEvent) => {
246+
e.preventDefault();
247+
typeof onDrop === "function" && onDrop();
248+
this.onDropFilesHandler({ e, onSuccess, onError });
249+
}
250+
};
251+
}
252+
})}
253+
254+
<input
255+
id={id || this.id}
256+
ref={ref => {
257+
if (ref) {
258+
this.input = ref;
259+
}
260+
}}
261+
accept={accept.join(",")}
262+
style={{ display: "none" }}
263+
type="file"
264+
multiple={multiple}
265+
onChange={e =>
266+
this.processSelectedFiles((e.target.files as any as Array<File>) ?? [])
267+
}
268+
/>
269+
</React.Fragment>
270+
);
271+
}
272+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { Files } from "./Files";
2+
3+
export default Files;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const generateId = () => {
2+
return "_" + Math.random().toString(36).substr(2, 9);
3+
};
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const readFileContent = async (file: File) => {
2+
return new Promise<string>((resolve, reject) => {
3+
const reader = new window.FileReader();
4+
reader.onload = function (e) {
5+
if (e.target) {
6+
resolve(e.target.result as string);
7+
} else {
8+
reject(`Unable to read file contents!`);
9+
}
10+
};
11+
12+
reader.readAsDataURL(file);
13+
});
14+
};

packages/project-utils/bundling/app/config/webpack.config.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ module.exports = function (webpackEnv, { paths, options }) {
223223
"react-dom$": require.resolve("react-dom/profiling"),
224224
"scheduler/tracing": require.resolve("scheduler/tracing-profiling")
225225
}),
226+
// This is a temporary fix, until we sort out the `react-butterfiles` dependency.
227+
"react-butterfiles": require.resolve("@webiny/app/react-butterfiles"),
226228
...(modules.webpackAliases || {})
227229
},
228230
fallback: {

packages/project-utils/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@
9898
"src": [
9999
"!!raw-loader!",
100100
"@material/base",
101+
"@webiny/app",
101102
"@webiny/api",
102103
"@webiny/tasks",
103104
"@webiny/handler",

0 commit comments

Comments
 (0)