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.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} (
+ {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} ({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.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} (
- {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} ({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).