I am trying to make the user download the pdf file that he has submitted on the form, the first time i click download, it downloads the pdf file but when i open it , it gives me Failed -network error on the file being downloaded, if i resave anything in the file while staying on the form, it downloads the file but when i open it , it gives me unable to load PDF file, then i resave again, and it downloads the pdf file and i actually open it. im not sure if this is related to how the action is updating the attachmentUrl,
Component:
import React, { useState } from "react";
import { MY_SUBMISSIONS_STATUS_ENUM } from "../../commmon/constants";
import {
FormControl,
InputLabel,
Select,
MenuItem,
TextField,
Button,
Box,
Typography,
FormControlLabel,
Checkbox,
Link,
} from "@mui/material";
import DownloadIcon from "@mui/icons-material/Download";
import * as MuiIcons from "../../commmon/MuiIcons";
import * as MuiStyles from "../../commmon/MuiStyles";
import * as MuiComponents from "../../commmon/MuiComponents";
import { useNavigate } from "react-router-dom";
import { useDispatch, useSelector } from "react-redux";
import { getActiveEntitiesAction, getAllEntitiesAction } from "../../actions";
import InputStartIcon from "../../commmon/InputStartIcon";
import colors from "../../commmon/colors";
import {
getFormAttachmentAction,
getProjectDetailsAction,
} from "../../actions/projectDetails";
import requiredAsteriskStyle from "../../commmon/redAsterisk";
const ProjectDetailsForm = ({
formData,
handleChange,
handleNext,
status_id,
edit,
view,
setFormData,
}) => {
const [fileName, setFileName] = useState(
formData.file ? formData.file.name : ""
);
const [fileUrl, setFileUrl] = useState(null);
const filename = "sample.pdf";
const isApproved = Number(status_id) === MY_SUBMISSIONS_STATUS_ENUM.APPROVED;
const shouldDisable = view || (edit && isApproved);
React.useEffect(() => {
dispatch(getFormAttachmentAction(formData.name, filename));
}, []);
React.useEffect(() => {
if (attachmentUrl) {
setFileUrl(attachmentUrl);
}
}, [attachmentUrl]);
const handleDownload = () => {
if (fileUrl) {
const link = document.createElement("a");
link.href = fileUrl;
link.download = "sample.pdf";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
const handleFileChange = (e) => {
if (e.target.files && e.target.files[0]) {
const newFile = e.target.files[0];
setFileName(newFile.name);
handleChange(e);
} else {
setFileName("");
handleChange({ target: { name: "file", value: null, files: [] } });
}
};
const removeFile = () => {
setFileName("");
document.querySelector('input[name="file"]').value = "";
handleChange({ target: { name: "file", value: null, files: [] } });
};
const dispatch = useDispatch();
const navigate = useNavigate();
const {
currentUser,
projectDetails,
entityArray,
activeEntities,
attachmentUrl,
} = useSelector(
({ usersReducer, projectDetailsReducer, entityManagementReducer }) => ({
currentUser: usersReducer?.currentUser,
projectDetails: projectDetailsReducer?.projectDetails,
entityArray: entityManagementReducer?.entityArray,
activeEntities: entityManagementReducer?.activeEntities,
attachmentUrl: projectDetailsReducer?.attachmentUrl,
})
);
console.log(attachmentUrl, "fkfk");
const [showRequired, setShowRequired] = React.useState(false);
const [loading, setLoading] = React.useState(false);
React.useEffect(() => {
dispatch(getProjectDetailsAction());
dispatch(getAllEntitiesAction());
dispatch(getActiveEntitiesAction());
}, []);
React.useEffect(() => {
let { multipleEntities, entities } = hasMultipleActiveEntities();
if (!multipleEntities && entities?.[0]?.id) {
setFormData({ ...formData, entity_id: entities?.[0]?.id });
}
}, [activeEntities]);
function hasMultipleActiveEntities() {
let UserActiveEntities = activeEntities?.filter((entity) => {
if (currentUser?.user_entities?.includes(entity.id)) return true;
});
if (UserActiveEntities?.length > 1) {
return {
multipleEntities: true,
entities: UserActiveEntities,
};
}
return {
multipleEntities: false,
entities: UserActiveEntities,
};
}
function checkEntityFieldDisablitiy() {
let { multipleEntities, entities } = hasMultipleActiveEntities();
if (!multipleEntities) {
if (entities?.[0]?.id === projectDetails?.[0]?.entity_id) return true;
}
return false;
}
const formatNumber = (value) => {
const number = parseFloat(value.replace(/,/g, ""));
return isNaN(number) ? "" : number.toLocaleString();
};
return (
<Box
className="form-container"
sx={{ padding: 3, borderRadius: 2, boxShadow: 1 }}
>
<Typography
variant="h4"
align="center"
gutterBottom
fontSize="25px"
fontFamily="din-medium"
sx={{
backgroundColor: "#00afaa",
borderBottom: "3px solid #008e8e",
padding: "10px",
color: "#ffffff",
borderRadius: "10px",
textAlign: "center",
paddingBottom: "20px",
}}
>
Business Case
</Typography>
<form onSubmit={handleNext}>
<MuiComponents.Stack spacing={2}>
<TextField
name="name"
label="Name"
value={formData.name}
onChange={handleChange}
disabled={shouldDisable}
required
fullWidth
InputLabelProps={requiredAsteriskStyle}
/>
<MuiComponents.FormControl fullWidth>
<MuiComponents.InputLabel>Entity Name</MuiComponents.InputLabel>
<MuiComponents.Select
name="entity_id"
value={formData?.entity_id || ""}
onChange={handleChange}
fullWidth
renderValue={(selected) => {
let userActiveEntities = hasMultipleActiveEntities();
//auto select for add
if (selected == "") {
// 1 entity
if (!userActiveEntities?.multipleEntities)
return userActiveEntities?.entities[0]?.full_name;
//multiple entities
else
return <p style={{ color: "#b3b3b3" }}>Select entity...</p>;
} else if (selected !== "") {
return userActiveEntities?.entities?.find(
(entity) => entity?.id == selected
)?.full_name;
}
//edit && ((1 || more) entities)
if (edit) {
return entityArray?.find((entity) => entity?.id == selected)
?.full_name;
}
}}
error={Boolean(showRequired) && Boolean(!formData?.entity_id)}
input={
<MuiComponents.OutlinedInput
label="Entity name"
startAdornment={
<InputStartIcon
icon={<MuiIcons.LanguageIcon color={colors.primary} />}
/>
}
/>
}
disabled={
loading || shouldDisable || checkEntityFieldDisablitiy()
}
displayEmpty
>
{edit &&
hasMultipleActiveEntities().entities?.find(
(entity) => entity.id === projectDetails?.entity_id
) ? null : (
<MuiComponents.MenuItem
key={projectDetails?.entity_id}
value={projectDetails?.entity_id}
>
{projectDetails?.entity_name}
</MuiComponents.MenuItem>
)}
{hasMultipleActiveEntities().entities?.map((entity) => {
return (
<MuiComponents.MenuItem key={entity.id} value={entity.id}>
{entity.full_name}
</MuiComponents.MenuItem>
);
})}
</MuiComponents.Select>
</MuiComponents.FormControl>
<TextField
name="category"
label="Category"
value={formData.category}
onChange={handleChange}
disabled={view}
required
fullWidth
InputLabelProps={requiredAsteriskStyle}
/>
<TextField
name="projectDescription"
label="Project Description"
value={formData.projectDescription}
onChange={handleChange}
disabled={view}
required
multiline
rows={4}
fullWidth
InputLabelProps={requiredAsteriskStyle}
/>
<TextField
name="duration"
label="Duration"
value={formData.duration}
onChange={handleChange}
disabled={shouldDisable}
required
fullWidth
type="number"
InputLabelProps={requiredAsteriskStyle}
InputProps={{
endAdornment: (
<MuiComponents.InputAdornment position="end">
<Typography
sx={{
fontWeight: "bold",
color: "#000000",
}}
>
MONTHS
</Typography>
</MuiComponents.InputAdornment>
),
}}
/>
<FormControl fullWidth>
<InputLabel>Project Program</InputLabel>
<Select
name="projectProgram"
value={formData.projectProgram}
onChange={handleChange}
disabled={view}
required
label="Project Program"
>
<MenuItem value="Digital Port Operations">
Digital Port Operations
</MenuItem>
<MenuItem value="Digital Transformations">
Digital Transformations
</MenuItem>
<MenuItem value="Smart Ports">Smart Ports</MenuItem>
</Select>
</FormControl>
<TextField
name="startDate"
label="Start Date"
type="date"
value={formData.startDate}
onChange={handleChange}
disabled={view}
required
InputLabelProps={{ shrink: true }}
fullWidth
InputLabelProps={requiredAsteriskStyle}
/>
<TextField
name="endDate"
label="End Date"
type="date"
value={formData.endDate}
onChange={handleChange}
disabled={view}
required
InputLabelProps={{ shrink: true }}
fullWidth
InputLabelProps={requiredAsteriskStyle}
/>
<TextField
name="estimatedCost"
label="Estimated Cost"
value={formatNumber(formData.estimatedCost)}
onChange={handleChange}
disabled={shouldDisable}
required
fullWidth
InputProps={{
endAdornment: (
<MuiComponents.InputAdornment position="end">
<Typography
sx={{
fontWeight: "bold",
color: "#000000", // Black color
}}
>
SAR
</Typography>
</MuiComponents.InputAdornment>
),
}}
InputLabelProps={requiredAsteriskStyle}
/>
<Box>
<Typography variant="h6">Funding Category:</Typography>
<Box className="checkbox-group">
<FormControlLabel
control={
<Checkbox
name="fundingInternally"
checked={formData.fundingInternally}
onChange={handleChange}
disabled={shouldDisable}
readOnly={view}
/>
}
label="Internal"
/>
<FormControlLabel
control={
<Checkbox
name="fundingMinistry"
checked={formData.fundingMinistry}
onChange={handleChange}
disabled={shouldDisable}
/>
}
label="External"
/>
</Box>
</Box>
<FormControl fullWidth>
<InputLabel>Project Status</InputLabel>
<Select
name="projectStatus"
value={formData.projectStatus}
onChange={handleChange}
disabled={view}
required
label="Project Status"
>
<MenuItem value="">Select a status</MenuItem>
<MenuItem value="On-Track">On-Track</MenuItem>
<MenuItem value="Completed">Completed</MenuItem>
<MenuItem value="Delayed">Delayed</MenuItem>
<MenuItem value="On-Hold">On-Hold</MenuItem>
<MenuItem value="Rejected">Rejected</MenuItem>
</Select>
</FormControl>
<Box>
<label htmlFor="file-upload">
<Typography>Upload Supporting Documents (PDF):</Typography>
{view ? (
<Box mt={2} display="flex" alignItems="center">
<DownloadIcon sx={{ marginRight: 1 }} />
<Button
onClick={handleDownload}
color="inherit"
sx={{ display: "flex", alignItems: "center" }}
>
<Typography variant="body2">
{fileName || "Download File"}
</Typography>
</Button>
</Box>
) : (
<>
<input
id="file-upload"
type="file"
name="file"
accept=".pdf"
onChange={handleFileChange}
disabled={shouldDisable}
style={{ display: "none" }}
/>
<Button
variant="contained"
component="span"
disabled={shouldDisable}
sx={{ marginTop: 1 }}
>
Choose File
</Button>
{fileName && (
<Box mt={2} display="flex" alignItems="center">
<Typography variant="body2" sx={{ flexGrow: 1 }}>
{fileName}
</Typography>
<Button
type="button"
onClick={removeFile}
variant="outlined"
color="error"
size="small"
>
X
</Button>
</Box>
)}
</>
)}
</label>
</Box>
<Button
type="submit"
variant="contained"
color="primary"
disabled={isApproved}
sx={{ marginTop: 2 }}
>
Next
</Button>
</MuiComponents.Stack>
</form>
</Box>
);
};
export default ProjectDetailsForm;
Controller:
export const getFormAttachment = async (req, res, next) => {
const { formName, filename } = req.params;
// Log the parameters received
console.log("Received parameters:", { formName, filename });
// Construct the file path
const uploadsDir = path.join(__dirname, "../../server/uploads/business");
console.log(uploadsDir, "Uploads Directory");
const filePath = path.join(uploadsDir, formName, filename);
console.log(`Resolved file path: ${filePath}`);
// Check if file exists
fs.stat(filePath, (err, stats) => {
if (err) {
console.error("File does not exist:", err);
return res.status(404).send("File not found");
}
// Check if path is a file
if (!stats.isFile()) {
return res.status(404).send("Not a file");
}
res.setHeader("Content-Type", "application/pdf");
// Send file for download
res.sendFile(filePath);
});
};
Action:
export function getFormAttachmentAction(formName, filename) {
return function (dispatch) {
dispatch({ type: GET_FORM_ATTACHMENT_REQUESTED });
return axios
.get(`/api/formAttachment/${formName}/${filename}`, {
responseType: "blob",
})
.then(({ data }) => {
const fileURL = URL.createObjectURL(
new Blob([data], { type: "application/pdf" })
);
dispatch({
type: GET_FORM_ATTACHMENT_SUCCESS,
payload: fileURL,
});
console.log(fileURL);
})
.catch((err) => {
if (err?.response?.status === 401) {
dispatch(signOutAction());
}
dispatch({
type: GET_FORM_ATTACHMENT_FAILURE,
payload: getErrorMessage(err),
});
});
};
}
Im not sure why this happens , but i think its because the fileUrl is a blob at first in the action is yet not updated but after resaving and triggering again it actually opens the file itself