From 497bb4c79900b3e86f114bbaccbfd76a5742f95d Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 7 Apr 2026 15:32:45 +0100 Subject: [PATCH] Extract RenderFileUpload and FieldArrayForm --- components/elements/fields/fieldArrayForm.js | 45 ++ .../elements/fields/renderFileUpload.js | 448 ++++++++++++++++ components/elements/index.js | 493 +----------------- memory-bank/change-log.md | 27 + 4 files changed, 524 insertions(+), 489 deletions(-) create mode 100644 components/elements/fields/fieldArrayForm.js create mode 100644 components/elements/fields/renderFileUpload.js diff --git a/components/elements/fields/fieldArrayForm.js b/components/elements/fields/fieldArrayForm.js new file mode 100644 index 00000000..0728bcf7 --- /dev/null +++ b/components/elements/fields/fieldArrayForm.js @@ -0,0 +1,45 @@ +import React, { useEffect } from "react"; +import _ from "lodash"; +import { FieldArray, change } from "redux-form"; +import { useDispatch } from "react-redux"; +import { RenderSubFields } from "./renderSubFields"; + +export const FieldArrayForm = (props) => { + const { + name, + form, + formProps, + parentFieldShowOnValue, + parentField, + maxSubField + } = props; + + const dispatch = useDispatch(); + + var showIfHasParentShowValue = + parentField != false && + !_.isEmpty(formProps[form]) && + _.has(formProps[form].values, parentField) && + parentFieldShowOnValue.indexOf( + formProps[form].values[parentField].toString() + ) > -1; + + useEffect(() => { + if (!showIfHasParentShowValue) { + dispatch(change("appealForm", name, null)); + } + }, [dispatch, name, showIfHasParentShowValue]); + + return ( + <> + {" "} + {showIfHasParentShowValue && ( + + )} + + ); +}; diff --git a/components/elements/fields/renderFileUpload.js b/components/elements/fields/renderFileUpload.js new file mode 100644 index 00000000..5164cc59 --- /dev/null +++ b/components/elements/fields/renderFileUpload.js @@ -0,0 +1,448 @@ +import { JSONPath as jsonpath } from "jsonpath-plus"; +import React, { useMemo, useState } from "react"; +import useTranslation from "next-translate/useTranslation"; +import Link from "next/link"; +import Dropzone from "react-dropzone"; +import { + deleteBlob, + getFilesFromBlobHashed, + uploadSingleFile, + getFilesFromBlobproxy +} from "../../../actions/services/documentService"; +import { bytesToSize, getThumbnailIconByExtension } from "../../utils"; +import { + getDocumentTypePrefix, + getThumbnailIconByMimeType, + validateUploadFilename +} from "../helpers/fileUploadHelpers"; + +const baseStyle = { + display: "flex", + flexDirection: "column", + alignItems: "center", + padding: "20px", + borderWidth: 2, + borderRadius: 2, + borderColor: "#eeeeee", + borderStyle: "dashed", + backgroundColor: "#fafafa", + color: "#bdbdbd", + transition: "border .3s ease-in-out" +}; + +const activeStyle = { + borderColor: "#2196f3" +}; + +const acceptStyle = { + borderColor: "#00e676" +}; + +const rejectStyle = { + borderColor: "#ff1744" +}; + +export const RenderFileUpload = (field) => { + let files = field.input.value; + let { t } = useTranslation(); + + const style = useMemo( + () => ({ + ...baseStyle + // ...(isDragActive ? activeStyle : {}), + // ...(isDragAccept ? acceptStyle : {}), + // ...(isDragReject ? rejectStyle : {}), + }), + [] + // [isDragActive, isDragReject, isDragAccept] + ); + const [filesList, setFilesList] = useState([]); + + const thumbs = filesList.map((file) => ( +
+ {file.name} -{" "} + + {file.name} ({file.size / 1024}kb) + +
+ )); + + const filelistObj = field.fileList || {}; + + const blobList = jsonpath({ + path: "$..[?(@ && @.documentType=='" + field.documentTypeCode + "')]", + json: filelistObj, + eval: true + }); + //const blobList = filelistObj; + + const deleteThisBlob = async ( + containerName, + blobName, + deleteblobhash, + getblobshash, + casefolderID + ) => { + //console.log(containerName, blobName, deleteblobhash); + deleteBlob(containerName, blobName, deleteblobhash, casefolderID) + .then((data) => data) + .then(() => { + getFilesFromBlobHashed( + containerName, + getblobshash, + casefolderID + ).then((newfilelist) => field.setFilesForAppeal(newfilelist)); + }); + }; + + const removeFile = (file) => { + const newFilesArr = files.filter((user) => user.name != file.name); + files == newFilesArr; + field.input.onChange(newFilesArr); + }; + + function removeEmptyObjects(arr) { + return arr.filter((obj) => Object.keys(obj).length > 0); + } + + const [errorMessage, setErrorMessage] = useState(null); + const [rejectedFiles, setRejectedFiles] = useState([]); // Store rejected files + + const [completedUploadFiles, setCompletedUploadFiles] = useState(false); + const [uploadCountMessage, setUploadCountMessage] = useState(""); + const [totalUploadFiles, setTotalUploadFiles] = useState(0); + + const handleDropRejected = (fileRejections) => { + const errors = fileRejections + .map(({ file, errors }) => { + return errors.map((error) => { + if (error.code === "file-too-large") { + return `${file.name} ${t( + "newappeal:new-appeal-fileupload-file-error-filesize-label" + )}`; + } + if (error.code === "file-invalid-type") { + return `${file.name} ${t( + "newappeal:new-appeal-fileupload-file-error-invalid-type-label" + )}`; + } + if (error.code === "filename-invalid-chars") { + return ( + error.message || + `${file.name} ${t( + "newappeal:new-appeal-fileupload-file-error-invalid-filename-label" + )}` + ); + } + return `${file.name} ${t( + "newappeal:new-appeal-fileupload-file-error-invalid-label" + )}`; + }); + }) + .flat(); // Flatten the errors array + + setRejectedFiles(errors); // Store the error messages for rejected files + }; + + // Handle accepted files (clear error message when files are accepted) + const handleDropAccepted = () => { + setErrorMessage(null); // Clear any previous error messages when files are accepted + setRejectedFiles([]); // Clear any previously rejected file errorsss + }; + + return ( + <> + validateUploadFilename(file, t)} + onDrop={(acceptedFiles, fileRejections, e) => { + // build + display rejected messages (ALWAYS, even if some accepted) + if (fileRejections?.length) { + handleDropRejected(fileRejections); + } else { + // only clear rejections when nothing was rejected on this drop + setRejectedFiles([]); + setErrorMessage(null); + } + + // if nothing accepted, stop (prevents showing only "uploading" when all invalid) + if (!acceptedFiles || acceptedFiles.length === 0) { + setCompletedUploadFiles(true); // optional: depends on your UX + setUploadCountMessage(0); + setTotalUploadFiles(0); + return; + } + + // continue with your existing upload logic for accepted files + setCompletedUploadFiles(false); + + const renamedAcceptedFiles = acceptedFiles.map( + (file) => + new File( + [file], + `${getDocumentTypePrefix(field.documentTypeCode)}_-_${ + file.name + }`, + { type: file.type } + ) + ); + + field.input.onChange(renamedAcceptedFiles); + + if (typeof field.setFileCount === "function") { + field.setFileCount( + (field.uploadCount || 0) + + renamedAcceptedFiles.length + ); + } + + setUploadCountMessage(0); + setTotalUploadFiles(renamedAcceptedFiles.length); + + uploadSingleFile( + renamedAcceptedFiles, + field.containerID, + field.ticketnumber, + { + onChunkComplete: ({ cumulativeUploaded }) => { + setUploadCountMessage(cumulativeUploaded); + } + } + ) + .then((data) => { + let buildAppealFilesArray = []; + let values = field.form.appealForm.values; + let filesUploadObj = renamedAcceptedFiles; + + for (let key in filesUploadObj) { + typeof filesUploadObj[key].name !== + "undefined" && + buildAppealFilesArray.push({ + name: filesUploadObj[key].name, + size: filesUploadObj[key].size + }); + } + + function removeDuplicates(arr, key) { + const seen = new Set(); + return arr.filter((item) => { + const value = item[key]; + if (seen.has(value)) return false; + seen.add(value); + return true; + }); + } + + buildAppealFilesArray = values.hasOwnProperty( + "filesList" + ) + ? buildAppealFilesArray.concat(values.filesList) + : buildAppealFilesArray; + + buildAppealFilesArray = removeDuplicates( + buildAppealFilesArray, + "name" + ); + + Object.assign(values, { + filesList: buildAppealFilesArray + }); + + // keep your existing server-side invalid handling + if (data.invalidFiles?.length > 0) { + setRejectedFiles((prev) => [ + ...prev, + ...data.invalidFiles + ]); + setUploadCountMessage( + renamedAcceptedFiles.length - + data.invalidFiles.length + ); + } + + return getFilesFromBlobproxy( + field.containerID, + field.ticketnumber + ); + }) + .then((data) => { + field.setFilesForAppeal(data); + setCompletedUploadFiles(true); + }); + }} + > + {({ getRootProps, getInputProps }) => ( + <> +
+
+ {" "} + +
+ {t( + "newappeal:new-appeal-fileupload-drop-label" + )}{" "} + (Max 50 MB) +
+ + ( + {t( + "newappeal:new-appeal-fileupload-file-list-label" + )} + ) + +
+ {/* */} +
+ + )} +
+ {rejectedFiles.length > 0 && ( +
+
    + {rejectedFiles.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} + {totalUploadFiles > 0 && completedUploadFiles == false && ( +
+ {t("home:uploading-files-label", { + number: + totalUploadFiles > 0 + ? `${uploadCountMessage} of ${totalUploadFiles}` + : uploadCountMessage + })} +
+ )} + {completedUploadFiles > 0 && uploadCountMessage >= 0 && ( +
+ {t("home:completed-uploading-files-label", { + number: uploadCountMessage + })} +
+ )} + {field.meta.touched && field.meta.error && ( + {field.meta.error} + )} + {files && Array.isArray(files) && ( + <> + {/* {(files = removeEmptyObjects(files))} */} + + {!completedUploadFiles && + files.map( + (file, i) => + typeof file.name != "undefined" && ( +
+ {" "} + {/* removeFile(file)} + > + − + */} + {file.name} + + {file.name} ( + {bytesToSize(file.size)})
+
+ {completedUploadFiles ? ( +

+ Upload complete +

+ ) : ( +

+ {t( + "home:uploading-files-progress-label" + )}{" "} +

+ )} +
+
+
+ ) + )} + + )} + {blobList.length > 0 && ( +

{t("newappeal:previously-added-files")}

+ )} + {blobList.map( + (blob, i) => + blob.documentType == field.documentTypeCode && ( +
+
+ { + setUploadCountMessage( + uploadCountMessage - 1 + ); + deleteThisBlob( + field.containerID, + blob.name, + blob.hasheddeletepath, + blob.hashgetblobs, + field.ticketnumber + ); + }} + title={t("home:remove-this-file-label")} + > + − + +
+ {blob.name} + + + {blob.name} ({bytesToSize(blob.contentLength)}) + +
+ ) + )}{" "} + + ); +}; diff --git a/components/elements/index.js b/components/elements/index.js index 89475e27..339866e1 100644 --- a/components/elements/index.js +++ b/components/elements/index.js @@ -1,36 +1,20 @@ -import { JSONPath as jsonpath } from "jsonpath-plus"; import axios from "axios"; import _ from "lodash"; import useTranslation from "next-translate/useTranslation"; -import Link from "next/link"; import { useRouter } from "next/router"; -import React, { useEffect, useMemo, useState } from "react"; +import React, { useEffect } from "react"; import "react-datepicker/dist/react-datepicker.css"; -import Dropzone from "react-dropzone"; -import { Field, FieldArray, change } from "redux-form"; -import { - deleteBlob, - getFilesFromBlobHashed, - uploadSingleFile, - getFilesFromBlobproxy -} from "../../actions/services/documentService"; +import { Field } from "redux-form"; import fieldLookup from "../../data/crmfieldlookuptranslations.json"; import pickListLookup from "../../data/picklistLookups.json"; -import { bytesToSize, getThumbnailIconByExtension } from "../utils"; import { setFileCount } from "../../store/appealType/action"; import "react-quill-new/dist/quill.snow.css"; import { useStore as store, useSelector } from "react-redux"; -import { useDispatch } from "react-redux"; import { validateField } from "./validationUtils"; // Import the validation function -import { - getDocumentTypePrefix, - getThumbnailIconByMimeType, - validateUploadFilename -} from "./helpers/fileUploadHelpers"; import { getFieldTranslation } from "./helpers/translationHelpers"; import { RenderRichMultiline } from "./fields/renderRichMultiline"; import { RenderTextfield } from "./fields/renderTextfield"; @@ -41,7 +25,8 @@ import { RenderRadio } from "./fields/renderRadio"; import { RenderCheckBox } from "./fields/renderCheckBox"; import { RenderPickList } from "./fields/renderPickList"; import { RenderDecimalField } from "./fields/renderDecimalField"; -import { RenderSubFields } from "./fields/renderSubFields"; +import { RenderFileUpload } from "./fields/renderFileUpload"; +import { FieldArrayForm } from "./fields/fieldArrayForm"; export function Textfield(props) { const { @@ -1145,473 +1130,3 @@ export function FileUploadField(props) { ); } -const baseStyle = { - display: "flex", - flexDirection: "column", - alignItems: "center", - padding: "20px", - borderWidth: 2, - borderRadius: 2, - borderColor: "#eeeeee", - borderStyle: "dashed", - backgroundColor: "#fafafa", - color: "#bdbdbd", - transition: "border .3s ease-in-out" -}; - -const activeStyle = { - borderColor: "#2196f3" -}; - -const acceptStyle = { - borderColor: "#00e676" -}; - -const rejectStyle = { - borderColor: "#ff1744" -}; - -const RenderFileUpload = (field) => { - let files = field.input.value; - let { t } = useTranslation(); - - const style = useMemo( - () => ({ - ...baseStyle - // ...(isDragActive ? activeStyle : {}), - // ...(isDragAccept ? acceptStyle : {}), - // ...(isDragReject ? rejectStyle : {}), - }), - [] - // [isDragActive, isDragReject, isDragAccept] - ); - const [filesList, setFilesList] = useState([]); - - const thumbs = filesList.map((file) => ( -
- {file.name} -{" "} - - {file.name} ({file.size / 1024}kb) - -
- )); - - const filelistObj = field.fileList || {}; - - const blobList = jsonpath({ - path: "$..[?(@ && @.documentType=='" + field.documentTypeCode + "')]", - json: filelistObj, - eval: true - }); - //const blobList = filelistObj; - - const deleteThisBlob = async ( - containerName, - blobName, - deleteblobhash, - getblobshash, - casefolderID - ) => { - //console.log(containerName, blobName, deleteblobhash); - deleteBlob(containerName, blobName, deleteblobhash, casefolderID) - .then((data) => data) - .then(() => { - getFilesFromBlobHashed( - containerName, - getblobshash, - casefolderID - ).then((newfilelist) => field.setFilesForAppeal(newfilelist)); - }); - }; - - const removeFile = (file) => { - const newFilesArr = files.filter((user) => user.name != file.name); - files == newFilesArr; - field.input.onChange(newFilesArr); - }; - - function removeEmptyObjects(arr) { - return arr.filter((obj) => Object.keys(obj).length > 0); - } - - const [errorMessage, setErrorMessage] = useState(null); - const [rejectedFiles, setRejectedFiles] = useState([]); // Store rejected files - - const [completedUploadFiles, setCompletedUploadFiles] = useState(false); - const [uploadCountMessage, setUploadCountMessage] = useState(""); - const [totalUploadFiles, setTotalUploadFiles] = useState(0); - - const handleDropRejected = (fileRejections) => { - const errors = fileRejections - .map(({ file, errors }) => { - return errors.map((error) => { - if (error.code === "file-too-large") { - return `${file.name} ${t( - "newappeal:new-appeal-fileupload-file-error-filesize-label" - )}`; - } - if (error.code === "file-invalid-type") { - return `${file.name} ${t( - "newappeal:new-appeal-fileupload-file-error-invalid-type-label" - )}`; - } - if (error.code === "filename-invalid-chars") { - return ( - error.message || - `${file.name} ${t( - "newappeal:new-appeal-fileupload-file-error-invalid-filename-label" - )}` - ); - } - return `${file.name} ${t( - "newappeal:new-appeal-fileupload-file-error-invalid-label" - )}`; - }); - }) - .flat(); // Flatten the errors array - - setRejectedFiles(errors); // Store the error messages for rejected files - }; - - // Handle accepted files (clear error message when files are accepted) - const handleDropAccepted = () => { - setErrorMessage(null); // Clear any previous error messages when files are accepted - setRejectedFiles([]); // Clear any previously rejected file errorsss - }; - - return ( - <> - validateUploadFilename(file, t)} - onDrop={(acceptedFiles, fileRejections, e) => { - // build + display rejected messages (ALWAYS, even if some accepted) - if (fileRejections?.length) { - handleDropRejected(fileRejections); - } else { - // only clear rejections when nothing was rejected on this drop - setRejectedFiles([]); - setErrorMessage(null); - } - - // if nothing accepted, stop (prevents showing only "uploading" when all invalid) - if (!acceptedFiles || acceptedFiles.length === 0) { - setCompletedUploadFiles(true); // optional: depends on your UX - setUploadCountMessage(0); - setTotalUploadFiles(0); - return; - } - - // continue with your existing upload logic for accepted files - setCompletedUploadFiles(false); - - const renamedAcceptedFiles = acceptedFiles.map( - (file) => - new File( - [file], - `${getDocumentTypePrefix(field.documentTypeCode)}_-_${ - file.name - }`, - { type: file.type } - ) - ); - - field.input.onChange(renamedAcceptedFiles); - - if (typeof field.setFileCount === "function") { - field.setFileCount( - (field.uploadCount || 0) + - renamedAcceptedFiles.length - ); - } - - setUploadCountMessage(0); - setTotalUploadFiles(renamedAcceptedFiles.length); - - uploadSingleFile( - renamedAcceptedFiles, - field.containerID, - field.ticketnumber, - { - onChunkComplete: ({ cumulativeUploaded }) => { - setUploadCountMessage(cumulativeUploaded); - } - } - ) - .then((data) => { - let buildAppealFilesArray = []; - let values = field.form.appealForm.values; - let filesUploadObj = renamedAcceptedFiles; - - for (let key in filesUploadObj) { - typeof filesUploadObj[key].name !== - "undefined" && - buildAppealFilesArray.push({ - name: filesUploadObj[key].name, - size: filesUploadObj[key].size - }); - } - - function removeDuplicates(arr, key) { - const seen = new Set(); - return arr.filter((item) => { - const value = item[key]; - if (seen.has(value)) return false; - seen.add(value); - return true; - }); - } - - buildAppealFilesArray = values.hasOwnProperty( - "filesList" - ) - ? buildAppealFilesArray.concat(values.filesList) - : buildAppealFilesArray; - - buildAppealFilesArray = removeDuplicates( - buildAppealFilesArray, - "name" - ); - - Object.assign(values, { - filesList: buildAppealFilesArray - }); - - // keep your existing server-side invalid handling - if (data.invalidFiles?.length > 0) { - setRejectedFiles((prev) => [ - ...prev, - ...data.invalidFiles - ]); - setUploadCountMessage( - renamedAcceptedFiles.length - - data.invalidFiles.length - ); - } - - return getFilesFromBlobproxy( - field.containerID, - field.ticketnumber - ); - }) - .then((data) => { - field.setFilesForAppeal(data); - setCompletedUploadFiles(true); - }); - }} - > - {({ getRootProps, getInputProps }) => ( - <> -
-
- {" "} - -
- {t( - "newappeal:new-appeal-fileupload-drop-label" - )}{" "} - (Max 50 MB) -
- - ( - {t( - "newappeal:new-appeal-fileupload-file-list-label" - )} - ) - -
- {/* */} -
- - )} -
- {rejectedFiles.length > 0 && ( -
-
    - {rejectedFiles.map((error, index) => ( -
  • {error}
  • - ))} -
-
- )} - {totalUploadFiles > 0 && completedUploadFiles == false && ( -
- {t("home:uploading-files-label", { - number: - totalUploadFiles > 0 - ? `${uploadCountMessage} of ${totalUploadFiles}` - : uploadCountMessage - })} -
- )} - {completedUploadFiles > 0 && uploadCountMessage >= 0 && ( -
- {t("home:completed-uploading-files-label", { - number: uploadCountMessage - })} -
- )} - {field.meta.touched && field.meta.error && ( - {field.meta.error} - )} - {files && Array.isArray(files) && ( - <> - {/* {(files = removeEmptyObjects(files))} */} - - {!completedUploadFiles && - files.map( - (file, i) => - typeof file.name != "undefined" && ( -
- {" "} - {/* removeFile(file)} - > - − - */} - {file.name} - - {file.name} ( - {bytesToSize(file.size)})
-
- {completedUploadFiles ? ( -

- Upload complete -

- ) : ( -

- {t( - "home:uploading-files-progress-label" - )}{" "} -

- )} -
-
-
- ) - )} - - )} - {blobList.length > 0 && ( -

{t("newappeal:previously-added-files")}

- )} - {blobList.map( - (blob, i) => - blob.documentType == field.documentTypeCode && ( -
-
- { - setUploadCountMessage( - uploadCountMessage - 1 - ); - deleteThisBlob( - field.containerID, - blob.name, - blob.hasheddeletepath, - blob.hashgetblobs, - field.ticketnumber - ); - }} - title={t("home:remove-this-file-label")} - > - − - -
- {blob.name} - - - {blob.name} ({bytesToSize(blob.contentLength)}) - -
- ) - )}{" "} - - ); -}; - -export const FieldArrayForm = (props) => { - const { - name, - form, - formProps, - parentFieldShowOnValue, - parentField, - maxSubField - } = props; - - const dispatch = useDispatch(); // Get dispatch using useDispatch hook - - var showIfHasParentShowValue = - parentField != false && - !_.isEmpty(formProps[form]) && - _.has(formProps[form].values, parentField) && - parentFieldShowOnValue.indexOf( - formProps[form].values[parentField].toString() - ) > -1; - - useEffect(() => { - if (!showIfHasParentShowValue) { - dispatch(change("appealForm", name, null)); - } - }, [dispatch, name, showIfHasParentShowValue]); - - return ( - <> - {" "} - {showIfHasParentShowValue && ( - - )} - - ); -}; diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md index bad50fa8..b3d55e9f 100644 --- a/memory-bank/change-log.md +++ b/memory-bank/change-log.md @@ -3528,3 +3528,30 @@ Validation: Follow-ups: - Continue Phase 2 with one bounded slice at a time; next low-risk target can be similar dead-code/no-op cleanup in another isolated renderer block. + +--- + +### CL-099: 22500 `components/elements/index.js` bounded extraction bundle (`RenderFileUpload` + `FieldArrayForm`) + +date: 2026-04-07 +author: Cline +scope: `components/elements/index.js`, `components/elements/fields/renderFileUpload.js`, `components/elements/fields/fieldArrayForm.js` +type: change +rationale: Execute a slightly larger but still bounded Phase 2 slice by extracting two self-contained blocks from the monolith (`RenderFileUpload` and `FieldArrayForm`) into dedicated field modules. +impact: No intended behavior change; preserves EN/CY output, upload flow, and accessibility semantics while reducing `index.js` size/coupling. +status: completed + +Summary: + +- Added `components/elements/fields/renderFileUpload.js` and moved the full existing `RenderFileUpload` implementation unchanged. +- Added `components/elements/fields/fieldArrayForm.js` and moved the full existing `FieldArrayForm` implementation unchanged. +- Updated `components/elements/index.js` imports to consume extracted modules. +- Removed inline `RenderFileUpload`/`FieldArrayForm` implementations and related now-unused imports/constants from `index.js`. + +Validation: + +- `npx eslint components/elements/index.js components/elements/fields/renderFileUpload.js components/elements/fields/fieldArrayForm.js` -> pass + +Follow-ups: + +- Continue Phase 2 with bounded renderer/module extractions from `components/elements/index.js` (one cohesive bundle per commit).