So I am creating a webapp where I am using Material-UI DataGrid. Expense.js below is the main page I am trying to render. Inside it, I have used three different components. The Custom Data Grid component, the Loading component, and a Custom Upload Button which I am rendering inside each DataGrid row in a column.
Now the Upload Button uploads a file to Drive and tries to add its URL to another column. I fetch the data initially to fill the Datagrid from the server. If I click on the Upload Button for the prefetched data, it works well. It sets the URL to another column.
If I add a row dynamically on the UI and then try to upload a file, it doesn't set the URL in the other column. I can see API request succeeding and rows being updated. But it doesn't show up on the UI.
Strange part is, if I don't split these components into three separate components and add everything inside the main Expense component as nested components, it all works well. The code below is a bit lengthy, I have tried to keep it readable.
Expense.js
const [snackbar, setSnackbar] = useState(null);
const [isLoading, setLoading] = useState(true);
const [rowModesModel, setRowModesModel] = useState({});
const [rows, setRows] = useState([]);
const columns = [
{ field: 'id', headerName: 'ID', width: 50 },
{ field: 'projectId', type: 'number', headerName: 'Project ID', width: 150, editable: true },
{ field: 'dueId', headerName: 'Due ID', width: 150 },
{ field: 'description', headerName: 'Description', width: 150, editable: true },
{
field: 'expenseDate', type: 'date', headerName: 'Expense Date', width: 150, editable: true, valueGetter: (params) => {
return new Date(params.value)
}
},
{ field: 'amount', type: 'number', headerName: 'Amount', width: 150, editable: true },
{
field: 'document', headerName: 'Document Link', width: 150,
// renderCell: renderLink,
editable: true,
},
{
headerName: 'Upload Document', width: 150,
renderCell: (params) => <UploadButton params={params} ------> UPLOAD BUTTON COMPONENT
setRows={setRows} rows={rows} setLoading={setLoading} setRowModesModel={setRowModesModel} />,
},
{ field: 'approved', type: 'boolean', headerName: 'Approved', width: 150, editable: true },
{ field: 'reimburesed', type: 'boolean', headerName: 'Reimbursed', width: 150, editable: true },
]
return (
<div>
{isLoading && (<Loading />)}
<DataGridCustom
customColumns={columns}
getUrl={EXPENSE_LIST_URI}
createUrl={CREATE_EXPENSE_URI}
updateUrl={UPDATE_EXPENSE_URI}
rowModesModel={rowModesModel}
setRowModesModel={setRowModesModel}
setLoading={setLoading}
setSnackbar={setSnackbar}
rows={rows}
setRows={setRows} />
{!!snackbar && (
<SnackBarCustom snackbar={snackbar} setSnackbar={setSnackbar} />
)}
</div>
);
}
Upload_Button.js
const VisuallyHiddenInput = styled('input')({
clip: 'rect(0 0 0 0)',
clipPath: 'inset(50%)',
height: 1,
overflow: 'hidden',
position: 'absolute',
bottom: 0,
left: 0,
whiteSpace: 'nowrap',
width: 1,
});
export default function UploadButton({params, setLoading, setRowModesModel, setRows, rows}) {
const { userName, userEmail, userRole } = useContext(UserContext)
function handleChange(event) {
setLoading(true)
const request = new XMLHttpRequest();
const url = process.env.REACT_APP_SERVER_HOST + USER_GOOGLE_TOKEN_URI
request.open("GET", url, false); // `false` makes the request synchronous
request.withCredentials = true;
request.send();
const res = JSON.parse(request.responseText)
const file = event.target.files[0]
const formData = new FormData();
const metadata = {
name: userEmail + "_" + Date.now() + "_" + file.name,
mimeType: file.type,
parents: [EXPENSES_FOLDER_ID],
response: 'file.id'
}
formData.append('metadata', new Blob([JSON.stringify(metadata)], {
type: "application/json"
}))
formData.append("file", file)
fetch(GOOGLE_DRIVE_UPLOAD_FILE_URI, {
method: 'POST',
headers: new Headers({ 'Authorization': 'Bearer ' + res.accessToken }),
body: formData
}).then((res) => {
return res.json();
}).then(function (val) {
const fileUrl = DRIVE_FILE_URI + val.id
console.log("Params")
console.log(params)
setRowModesModel((oldModel) => ({
...oldModel,
[params.id]: { mode: GridRowModes.Edit, fieldToFocus: 'document' },
}));
setRows(rows.map((row) => (row.id === params.id ? {...row, document: fileUrl} : row))); ----------> Setting Row here
setLoading(false);
});
}
return (
<Button size="small" component="label" variant="contained" startIcon={<CloudUploadIcon />}>
Upload file
<VisuallyHiddenInput type="file" onChange={handleChange} />
</Button>
);
Custom_Data_Grid.js
export default function DataGridCustom({ customColumns, customPermissions, getUrl,
createUrl, updateUrl, rowModesModel, setRowModesModel, setLoading, setSnackbar, rows, setRows }) {
console.log("rendering data grid")
console.log("rows")
console.log(rows)
function guidGenerator() {
var S4 = function () {
return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
};
return (S4() + S4() + "-" + S4() + "-" + S4() + "-" + S4() + "-" + S4() + S4() + S4());
}
const { userName, userEmail, userRole } = useContext(UserContext)
function checkPermission(action) {
return customPermissions[action] === "admin" ? true : false
}
console.log({ customColumns })
const columns = [
...customColumns,
{
field: 'actions',
type: 'actions',
headerName: 'Actions',
width: 100,
cellClassName: 'actions',
getActions: ({ id }) => {
const isInEditMode = rowModesModel[id]?.mode === GridRowModes.Edit;
if (isInEditMode) {
return [
<GridActionsCellItem
icon={<SaveIcon />}
label="Save"
sx={{
color: 'primary.main',
}}
onClick={handleSaveClick(id)}
disabled={userRole !== "admin"}
/>,
<GridActionsCellItem
icon={<CancelIcon />}
label="Cancel"
className="textPrimary"
onClick={handleCancelClick(id)}
color="inherit"
disabled={userRole !== "admin"}
/>,
];
}
return [
<GridActionsCellItem
icon={<EditIcon />}
label="Edit"
className="textPrimary"
onClick={handleEditClick(id)}
color="inherit"
disabled={userRole !== "admin"}
/>,
<GridActionsCellItem
icon={<DeleteIcon />}
label="Delete"
onClick={handleDeleteClick(id)}
color="inherit"
disabled
/>,
];
},
},
];
useEffect(() => {
fetch(process.env.REACT_APP_SERVER_HOST + getUrl, {
credentials: "include"
})
.then((response) => response.json())
.then((data) => {
console.log(data);
setRows(data);
setLoading(false);
}).catch((error) => {
console.log(error.message);
handleProcessRowUpdateError(error)
});
}, []);
const makeRequest = (data) => {
const request = new XMLHttpRequest();
if (data.isNew) {
const url = process.env.REACT_APP_SERVER_HOST + createUrl
request.open("POST", url, false); // `false` makes the request synchronous
delete data.id;
}
else {
const url = process.env.REACT_APP_SERVER_HOST + updateUrl
request.open("PUT", url, false); // `false` makes the request synchronous
}
request.setRequestHeader("Content-Type", "application/json");
delete data.isNew;
request.withCredentials = true;
request.send(JSON.stringify(data));
return JSON.parse(request.responseText)
}
function AddToolbar(props) {
const { setRows, setRowModesModel } = props;
const handleClick = () => {
const id = guidGenerator()
setRows((oldRows) => [...oldRows, { id, isNew: true }]);
setRowModesModel((oldModel) => ({
...oldModel,
[id]: { mode: GridRowModes.Edit, fieldToFocus: 'name' },
}));
};
return (
<GridToolbarContainer>
<Button color="primary" startIcon={<AddIcon />} onClick={handleClick} disabled={userRole === "user"}>
Add Record
</Button>
</GridToolbarContainer>
);
}
const handleRowEditStop = (params, event) => {
if (params.reason === GridRowEditStopReasons.rowFocusOut) {
event.defaultMuiPrevented = true;
}
};
const handleEditClick = (id) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.Edit } });
};
const handleSaveClick = (id) => () => {
setRowModesModel({ ...rowModesModel, [id]: { mode: GridRowModes.View } });
};
const handleDeleteClick = (id) => () => {
setRows(rows.filter((row) => row.id !== id));
};
const handleCancelClick = (id) => () => {
setRowModesModel({
...rowModesModel,
[id]: { mode: GridRowModes.View, ignoreModifications: true },
});
const editedRow = rows.find((row) => row.id === id);
if (editedRow.isNew) {
setRows(rows.filter((row) => row.id !== id));
}
};
const processRowUpdate = (newRow) => {
setLoading(true)
console.log("New Row")
console.log(newRow)
const oldId = newRow.id
const savedRow = makeRequest(newRow);
setRows(rows.map((row) => (row.id === oldId ? savedRow : row)));
setLoading(false)
return savedRow;
};
const handleRowModesModelChange = (newRowModesModel) => {
setRowModesModel(newRowModesModel);
};
const handleProcessRowUpdateError = useCallback((error) => {
setLoading(false)
setSnackbar({ children: error.message, severity: 'error' });
}, []);
return (
<div>
<Box
sx={{
height: 500,
width: '100%',
'& .actions': {
color: 'text.secondary',
},
'& .textPrimary': {
color: 'text.primary',
},
}}
>
<DataGrid
rows={rows}
columns={columns}
editMode="row"
rowModesModel={rowModesModel}
onRowModesModelChange={handleRowModesModelChange}
onRowEditStop={handleRowEditStop}
processRowUpdate={processRowUpdate}
onProcessRowUpdateError={handleProcessRowUpdateError}
slots={{
toolbar: AddToolbar,
}}
slotProps={{
toolbar: { setRows, setRowModesModel },
}}
/>
</Box>
</div>
);
}