Skip to content

Commit 338673d

Browse files
React: Implement FileUploader in DataGrid edit form with modern TypeScript patterns
- Implement proper TypeScript typing with DataGridTypes and Employee interface - Use modern React patterns: useCallback, memo, proper ref handling - Add FileUploaderEditor and FileUploaderWithPreview components - Add error handling and retry functionality - Include proper CSS styling for uploaded images - Remove old implementation files and use clean modern structure - Add DevExtreme CSS import to main.tsx
1 parent 87fc319 commit 338673d

13 files changed

+226
-225
lines changed

React/src/App.css

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,22 @@
11
.main {
22
margin: 50px;
33
width: 90vw;
4+
}
5+
6+
.uploadedImage {
7+
max-width: 100px;
8+
max-height: 100px;
9+
border: 1px solid #ddd;
10+
border-radius: 4px;
11+
margin-bottom: 10px;
12+
display: block;
13+
}
14+
15+
.retryButton {
16+
margin-top: 10px;
17+
}
18+
19+
.dx-datagrid-edit-form .uploadedImage {
20+
max-width: 150px;
21+
max-height: 150px;
422
}

React/src/App.tsx

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,77 @@
1-
import { useCallback, useState } from 'react';
1+
import { useState, useCallback } from 'react';
2+
import DataGrid, { type DataGridTypes } from 'devextreme-react/data-grid';
3+
import { Column, Editing, Popup, Form } from 'devextreme-react/data-grid';
4+
import { Item } from 'devextreme-react/form';
5+
import { useEvent } from './utils';
6+
import { cellRender, FileUploaderEditor } from './FileUploaderEditor';
7+
import { employees, type Employee } from './data';
28
import './App.css';
3-
import 'devextreme/dist/css/dx.material.blue.light.compact.css';
4-
import Button from 'devextreme-react/button';
59

610
function App(): JSX.Element {
7-
var [count, setCount] = useState<number>(0);
8-
const clickHandler = useCallback(() => {
9-
setCount((prev) => prev + 1);
10-
}, [setCount]);
11+
const [retryButtonVisible, setRetryButtonVisible] = useState<boolean>(false);
12+
13+
const onEditCanceled = useEvent((_e: DataGridTypes.EditCanceledEvent<Employee, number>): void => {
14+
if (retryButtonVisible) {
15+
setRetryButtonVisible(false);
16+
}
17+
});
18+
19+
const onSaved = useEvent((_e: DataGridTypes.SavedEvent<Employee, number>): void => {
20+
if (retryButtonVisible) {
21+
setRetryButtonVisible(false);
22+
}
23+
});
24+
25+
const editCellRender = useCallback(
26+
(cellInfo: DataGridTypes.ColumnEditCellTemplateData<Employee, number>): JSX.Element => (
27+
<FileUploaderEditor
28+
cellInfo={cellInfo}
29+
retryButtonVisible={retryButtonVisible}
30+
setRetryButtonVisible={setRetryButtonVisible}
31+
/>
32+
),
33+
[retryButtonVisible]
34+
);
35+
1136
return (
12-
<div className="main">
13-
<Button text={`Click count: ${count}`} onClick={clickHandler} />
14-
</div>
37+
<DataGrid
38+
id="gridContainer"
39+
dataSource={employees}
40+
keyExpr="ID"
41+
showBorders={true}
42+
onEditCanceled={onEditCanceled}
43+
onSaved={onSaved}
44+
>
45+
<Editing mode="popup" allowUpdating={true}>
46+
<Popup title="Employee Info" showTitle={true} width={700} />
47+
<Form>
48+
<Item itemType="group" colCount={2} colSpan={2}>
49+
<Item dataField="Prefix" />
50+
<Item dataField="FirstName" />
51+
<Item dataField="LastName" />
52+
<Item dataField="Position" />
53+
<Item dataField="BirthDate" />
54+
<Item dataField="HireDate" />
55+
</Item>
56+
<Item itemType="group" caption="Photo" colCount={2} colSpan={2}>
57+
<Item dataField="Picture" colSpan={2} />
58+
</Item>
59+
</Form>
60+
</Editing>
61+
<Column
62+
dataField="Picture"
63+
width={70}
64+
allowSorting={false}
65+
cellRender={cellRender}
66+
editCellRender={editCellRender}
67+
/>
68+
<Column dataField="Prefix" width={70} caption="Title" />
69+
<Column dataField="FirstName" />
70+
<Column dataField="LastName" />
71+
<Column dataField="Position" />
72+
<Column dataField="BirthDate" dataType="date" />
73+
<Column dataField="HireDate" dataType="date" />
74+
</DataGrid>
1575
);
1676
}
1777

React/src/FileUploaderEditor.tsx

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React, { useRef, useCallback, type Dispatch, type SetStateAction } from "react";
2+
import { type FileUploaderRef } from "devextreme-react/file-uploader";
3+
import Button from "devextreme-react/button";
4+
import { FileUploaderWithPreview } from "./FileUploaderWithPreview";
5+
import { backendURL } from "./constants";
6+
import type { DataGridTypes } from "devextreme-react/data-grid";
7+
import type { Employee } from "./data";
8+
9+
interface FileUploaderEditorProps {
10+
cellInfo: DataGridTypes.ColumnEditCellTemplateData<Employee, number>;
11+
setRetryButtonVisible: Dispatch<SetStateAction<boolean>>;
12+
retryButtonVisible: boolean;
13+
}
14+
15+
export const cellRender = (data: DataGridTypes.ColumnCellTemplateData<Employee, number>): JSX.Element => {
16+
return <img src={backendURL + data.value} alt="employee pic" style={{ maxWidth: '100%', height: 'auto' }} />;
17+
};
18+
19+
export const FileUploaderEditor = React.memo<FileUploaderEditorProps>(({
20+
cellInfo,
21+
setRetryButtonVisible,
22+
retryButtonVisible
23+
}) => {
24+
const fileUploaderRef = useRef<FileUploaderRef>(null);
25+
26+
const onClick = useCallback((): void => {
27+
// The retry UI/API is not implemented. Use a private API as shown at T611719.
28+
const fileUploaderInstance = fileUploaderRef.current?.instance();
29+
if (fileUploaderInstance) {
30+
// @ts-expect-error: Accessing private API for retry functionality
31+
for (let i = 0; i < fileUploaderInstance._files.length; i++) {
32+
// @ts-expect-error: Accessing private API for retry functionality
33+
delete fileUploaderInstance._files[i].uploadStarted;
34+
}
35+
fileUploaderInstance.upload();
36+
}
37+
}, []);
38+
39+
return (
40+
<>
41+
<FileUploaderWithPreview
42+
setRetryButtonVisible={setRetryButtonVisible}
43+
cellInfo={cellInfo}
44+
fileUploaderRef={fileUploaderRef}
45+
/>
46+
<Button
47+
className="retryButton"
48+
text="Retry"
49+
visible={retryButtonVisible}
50+
onClick={onClick}
51+
/>
52+
</>
53+
);
54+
});
55+
56+
FileUploaderEditor.displayName = 'FileUploaderEditor';
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { useRef, type Dispatch, type SetStateAction, type RefObject, memo } from "react";
2+
import FileUploader, { type FileUploaderRef } from "devextreme-react/file-uploader";
3+
import { useEvent } from "./utils";
4+
import { backendURL } from "./constants";
5+
import type { FileUploaderTypes } from "devextreme-react/file-uploader";
6+
import type { DataGridTypes } from "devextreme-react/data-grid";
7+
import type { Employee } from "./data";
8+
9+
interface FileUploaderPreviewProps {
10+
cellInfo: DataGridTypes.ColumnEditCellTemplateData<Employee, number>;
11+
setRetryButtonVisible: Dispatch<SetStateAction<boolean>>;
12+
fileUploaderRef: RefObject<FileUploaderRef>;
13+
}
14+
15+
export const FileUploaderWithPreview = memo<FileUploaderPreviewProps>(({
16+
setRetryButtonVisible,
17+
cellInfo,
18+
fileUploaderRef
19+
}) => {
20+
const imgRef = useRef<HTMLImageElement>(null);
21+
22+
const onValueChanged = useEvent((e: FileUploaderTypes.ValueChangedEvent): void => {
23+
const files = e.value;
24+
if (files && files.length > 0) {
25+
const reader = new FileReader();
26+
reader.onload = function (args) {
27+
if (typeof args.target?.result === 'string' && imgRef.current) {
28+
imgRef.current.setAttribute('src', args.target.result);
29+
}
30+
};
31+
reader.readAsDataURL(files[0]); // convert to base64 string
32+
}
33+
});
34+
35+
const onUploaded = useEvent((e: FileUploaderTypes.UploadedEvent): void => {
36+
if (e.request?.responseText) {
37+
cellInfo.setValue("images/employees/" + e.request.responseText);
38+
setRetryButtonVisible(false);
39+
}
40+
});
41+
42+
const onUploadError = useEvent((e: FileUploaderTypes.UploadErrorEvent): void => {
43+
const xhttp = e.request;
44+
if (xhttp && xhttp.status === 400) {
45+
e.message = e.error?.responseText || "Upload error";
46+
}
47+
if (xhttp && xhttp.readyState === 4 && xhttp.status === 0) {
48+
e.message = "Connection refused";
49+
}
50+
setRetryButtonVisible(true);
51+
});
52+
53+
return (
54+
<>
55+
<img
56+
ref={imgRef}
57+
className="uploadedImage"
58+
src={`${backendURL}${cellInfo.value}`}
59+
alt="employee pic"
60+
style={{ maxWidth: '100%', height: 'auto', marginBottom: '10px' }}
61+
/>
62+
<FileUploader
63+
ref={fileUploaderRef}
64+
multiple={false}
65+
accept="image/*"
66+
uploadMode="instantly"
67+
uploadUrl={backendURL + "FileUpload/post"}
68+
onValueChanged={onValueChanged}
69+
onUploaded={onUploaded}
70+
onUploadError={onUploadError}
71+
/>
72+
</>
73+
);
74+
});
75+
76+
FileUploaderWithPreview.displayName = 'FileUploaderWithPreview';
File renamed without changes.

React/src/orig_data.ts renamed to React/src/data.ts

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,5 @@ export const employees: Employee[] = [{
9898
"BirthDate": "1985/06/05",
9999
"HireDate": "2008/03/24",
100100
"Notes": "Cindy joined us in 2008 and has been in the HR department for 2 years. \r\n\r\nShe was recently awarded employee of the month. Way to go Cindy!",
101-
"Address": "2211 Bonita Dr."
102-
}, {
103-
"ID": 30,
104-
"FirstName": "Kent",
105-
"LastName": "Samuelson",
106-
"Prefix": "Dr.",
107-
"Position": "Ombudsman",
108-
"Picture": "images/employees/02.png",
109-
"BirthDate": "1972/09/11",
110-
"HireDate": "2009/04/22",
111-
"Notes": "As our ombudsman, Kent is on the front-lines solving customer problems and helping our partners address issues out in the field. He is a classically trained musician and is a member of the Chamber Orchestra.",
112-
"Address": "12100 Mora Dr"
113-
}];
101+
"Address": "3800 S Lamar Blvd."
102+
}];

React/src/main.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { StrictMode } from 'react';
22
import { createRoot } from 'react-dom/client';
3+
import 'devextreme/dist/css/dx.material.blue.light.compact.css';
34
import './index.css';
45
import App from './App.tsx';
56

React/src/orig_App.tsx

Lines changed: 0 additions & 72 deletions
This file was deleted.

React/src/orig_FileUploaderEditor.tsx

Lines changed: 0 additions & 42 deletions
This file was deleted.

0 commit comments

Comments
 (0)