diff --git a/actions/services/documentDirectService.js b/actions/services/documentDirectService.js
index 6b1bc2df..b63b8cd3 100644
--- a/actions/services/documentDirectService.js
+++ b/actions/services/documentDirectService.js
@@ -14,6 +14,8 @@ import {
postSignedFileJson
} from "../clients/fileClient";
+const MAX_FILES_PER_UPLOAD_BATCH = 10;
+
export const getAwaitingSubmissionFromBlob = (containerName) => {
const route = buildFileQuery("/api/file/getawaitingsubmissionfromblob", {
container: containerName
@@ -130,21 +132,69 @@ export const uploadFiles = async (
}
};
-export const uploadSingleFile = async (filesObj, containerID, casefolderID) => {
+export const uploadSingleFile = async (
+ filesObj,
+ containerID,
+ casefolderID,
+ options = {}
+) => {
let files = filesObj;
-
- var formData = new FormData();
- formData.append("containerID", containerID);
- formData.append("casefolderID", casefolderID);
-
- for (let i = 0; i < files.length; i++) {
- formData.append(files[i].name, files[i]);
- }
-
- var queryUrl = "/api/file/uploadsinglefile";
-
try {
- return await postSignedFileJson(queryUrl, formData);
+ const queryUrl = "/api/file/uploadsinglefile";
+ const uploadResponse = {
+ data: [],
+ invalidFiles: []
+ };
+ let cumulativeUploaded = 0;
+ let cumulativeInvalid = 0;
+ const totalFiles = files.length;
+ const totalChunks = Math.ceil(totalFiles / MAX_FILES_PER_UPLOAD_BATCH);
+
+ for (
+ let index = 0;
+ index < files.length;
+ index += MAX_FILES_PER_UPLOAD_BATCH
+ ) {
+ const chunkIndex =
+ Math.floor(index / MAX_FILES_PER_UPLOAD_BATCH) + 1;
+ const fileChunk = files.slice(
+ index,
+ index + MAX_FILES_PER_UPLOAD_BATCH
+ );
+ const formData = new FormData();
+
+ formData.append("containerID", containerID);
+ formData.append("casefolderID", casefolderID);
+
+ for (let i = 0; i < fileChunk.length; i++) {
+ formData.append(fileChunk[i].name, fileChunk[i]);
+ }
+
+ const chunkResult = await postSignedFileJson(queryUrl, formData);
+
+ if (Array.isArray(chunkResult?.data)) {
+ uploadResponse.data.push(...chunkResult.data);
+ cumulativeUploaded += chunkResult.data.length;
+ }
+
+ if (Array.isArray(chunkResult?.invalidFiles)) {
+ uploadResponse.invalidFiles.push(...chunkResult.invalidFiles);
+ cumulativeInvalid += chunkResult.invalidFiles.length;
+ }
+
+ if (typeof options.onChunkComplete === "function") {
+ options.onChunkComplete({
+ chunkIndex,
+ totalChunks,
+ totalFiles,
+ cumulativeUploaded,
+ cumulativeInvalid,
+ processedFiles: cumulativeUploaded + cumulativeInvalid
+ });
+ }
+ }
+
+ return uploadResponse;
} catch (error) {
consoleLogger(error);
}
diff --git a/components/case/representation/representationElements.js b/components/case/representation/representationElements.js
index b1c6deaf..d59ffda3 100644
--- a/components/case/representation/representationElements.js
+++ b/components/case/representation/representationElements.js
@@ -1061,6 +1061,7 @@ export const RenderFileUpload = (field) => {
const [completedUploadFiles, setCompletedUploadFiles] = useState(false);
const [uploadCountMessage, setUploadCountMessage] = useState("");
+ const [totalUploadFiles, setTotalUploadFiles] = useState(0);
const handleDropRejected = (fileRejections) => {
const errors = fileRejections
@@ -1169,6 +1170,7 @@ export const RenderFileUpload = (field) => {
if (!acceptedFiles || acceptedFiles.length === 0) {
setCompletedUploadFiles(true); // optional: depends on your UX
setUploadCountMessage(0);
+ setTotalUploadFiles(0);
return;
}
@@ -1206,7 +1208,8 @@ export const RenderFileUpload = (field) => {
);
}
- setUploadCountMessage(renamedAcceptedFiles.length);
+ setUploadCountMessage(0);
+ setTotalUploadFiles(renamedAcceptedFiles.length);
uploadSingleFile(
renamedAcceptedFiles,
@@ -1214,7 +1217,12 @@ export const RenderFileUpload = (field) => {
field.ticketnumber +
"/" +
field.filenamePrefix.representationForm.values
- .repfile_name
+ .repfile_name,
+ {
+ onChunkComplete: ({ cumulativeUploaded }) => {
+ setUploadCountMessage(cumulativeUploaded);
+ }
+ }
).then((data) => {
console.log(data);
let buildAppealFilesArray = [];
@@ -1324,10 +1332,13 @@ export const RenderFileUpload = (field) => {
)}
- {uploadCountMessage > 0 && completedUploadFiles == false && (
+ {totalUploadFiles > 0 && completedUploadFiles == false && (
{t("home:uploading-files-label", {
- number: uploadCountMessage
+ number:
+ totalUploadFiles > 0
+ ? `${uploadCountMessage} of ${totalUploadFiles}`
+ : uploadCountMessage
})}
)}
diff --git a/components/elements/index.js b/components/elements/index.js
index ebe58c29..e86b88e7 100644
--- a/components/elements/index.js
+++ b/components/elements/index.js
@@ -2147,6 +2147,7 @@ const RenderFileUpload = (field) => {
const [completedUploadFiles, setCompletedUploadFiles] = useState(false);
const [uploadCountMessage, setUploadCountMessage] = useState("");
+ const [totalUploadFiles, setTotalUploadFiles] = useState(0);
const handleDropRejected = (fileRejections) => {
const errors = fileRejections
@@ -2254,6 +2255,7 @@ const RenderFileUpload = (field) => {
if (!acceptedFiles || acceptedFiles.length === 0) {
setCompletedUploadFiles(true); // optional: depends on your UX
setUploadCountMessage(0);
+ setTotalUploadFiles(0);
return;
}
@@ -2280,12 +2282,18 @@ const RenderFileUpload = (field) => {
);
}
- setUploadCountMessage(renamedAcceptedFiles.length);
+ setUploadCountMessage(0);
+ setTotalUploadFiles(renamedAcceptedFiles.length);
uploadSingleFile(
renamedAcceptedFiles,
field.containerID,
- field.ticketnumber
+ field.ticketnumber,
+ {
+ onChunkComplete: ({ cumulativeUploaded }) => {
+ setUploadCountMessage(cumulativeUploaded);
+ }
+ }
)
.then((data) => {
let buildAppealFilesArray = [];
@@ -2386,10 +2394,13 @@ const RenderFileUpload = (field) => {
)}
- {uploadCountMessage > 0 && completedUploadFiles == false && (
+ {totalUploadFiles > 0 && completedUploadFiles == false && (
{t("home:uploading-files-label", {
- number: uploadCountMessage
+ number:
+ totalUploadFiles > 0
+ ? `${uploadCountMessage} of ${totalUploadFiles}`
+ : uploadCountMessage
})}
)}
diff --git a/memory-bank/change-log.md b/memory-bank/change-log.md
index c6e4d9b9..54b008da 100644
--- a/memory-bank/change-log.md
+++ b/memory-bank/change-log.md
@@ -3194,3 +3194,122 @@ Validation:
Follow-ups:
- Optional next bounded slice: stop or switch scope; touched-function micro-tidies in this area are now largely exhausted.
+
+---
+
+### CL-090: uploadsinglefile API batch upload chunking (10 files per pass)
+
+date: 2026-04-02
+author: Cline
+scope: `pages/api/file/uploadsinglefile.js`
+type: change
+rationale: Prevent oversized single-pass uploads by splitting validated files into bounded batches and invoking existing upload logic per batch.
+impact: Upload flow now processes validated files in deterministic chunks of 10 while preserving existing hash guards, validation behavior, and response contract (`data`, `invalidFiles`).
+status: completed
+
+Summary:
+
+- Added `MAX_FILES_PER_UPLOAD_BATCH = 10` in `uploadsinglefile` API route.
+- Replaced single `uploadSingleFile(...)` invocation with chunked processing:
+ - converts allowed files object to entries
+ - slices into batches of 10
+ - awaits `uploadSingleFile(...)` once per batch
+ - aggregates batch results into one `data` array for response
+- Kept existing error handling unchanged (`UPLOAD_SINGLE_FILE_FAILED` on catch).
+
+Validation:
+
+- `npx eslint pages/api/file/uploadsinglefile.js` -> executed with no lint output.
+
+Follow-ups:
+
+- Optional: add a focused API contract/unit test to assert that 11+ files cause multiple `uploadSingleFile` invocations with max chunk size 10.
+
+---
+
+### CL-091: uploadsinglefile client-side batch upload chunking parity (10 files per pass)
+
+date: 2026-04-02
+author: Cline
+scope: `actions/services/documentDirectService.js`
+type: change
+rationale: Ensure client upload flow aligns with server batching requirement by splitting outbound `uploadsinglefile` requests into chunks of 10 files.
+impact: Client now sends multiple sequential API requests (max 10 files each) and merges per-chunk responses into one contract-compatible payload (`data`, `invalidFiles`) for existing UI handlers.
+status: completed
+
+Summary:
+
+- Added `MAX_FILES_PER_UPLOAD_BATCH = 10` to document direct service.
+- Updated `uploadSingleFile(filesObj, containerID, casefolderID)` to:
+ - split selected files into batches of 10
+ - create a fresh `FormData` per batch
+ - call `/api/file/uploadsinglefile` once per batch (sequential `await`)
+ - aggregate `data` and `invalidFiles` from all chunk responses
+- Preserved function return shape used by current upload UIs.
+
+Validation:
+
+- `npx eslint pages/api/file/uploadsinglefile.js actions/services/documentDirectService.js` -> executed with no lint output.
+
+Follow-ups:
+
+- Optional: add a focused service-behaviour test that verifies `uploadSingleFile` issues N requests for N/10 chunks and merges all chunk responses.
+
+---
+
+### CL-092: per-chunk upload UI progress updates (new appeal + representations)
+
+date: 2026-04-02
+author: Cline
+scope: `actions/services/documentDirectService.js`, `components/elements/index.js`, `components/case/representation/representationElements.js`
+type: change
+rationale: Update UX so upload progress reflects completed chunks incrementally instead of waiting for all chunk requests to finish.
+impact: Upload status message now updates after each chunk completes, showing users progressive completion for large multi-file uploads while preserving existing response/error contracts.
+status: completed
+
+Summary:
+
+- Extended `uploadSingleFile(...)` service signature with optional callback support:
+ - new `options` argument
+ - invokes `options.onChunkComplete(...)` after each chunk upload
+ - emits cumulative counters (`cumulativeUploaded`, `cumulativeInvalid`, `processedFiles`, chunk index metadata)
+- Wired per-chunk UI progress updates in both upload entry points:
+ - `components/elements/index.js`
+ - `components/case/representation/representationElements.js`
+- Both now pass `onChunkComplete` and update `uploadCountMessage` from cumulative uploaded count as each chunk finishes.
+
+Validation:
+
+- `npx eslint pages/api/file/uploadsinglefile.js actions/services/documentDirectService.js components/elements/index.js components/case/representation/representationElements.js` -> executed with no lint output.
+
+Follow-ups:
+
+- Optional: add a focused UI behaviour test to assert upload progress increments per completed chunk for >10 file uploads.
+
+---
+
+### CL-093: upload progress message format `X of Y` during chunked uploads
+
+date: 2026-04-02
+author: Cline
+scope: `components/elements/index.js`, `components/case/representation/representationElements.js`
+type: change
+rationale: User requested clearer in-progress feedback showing uploaded count relative to total files (e.g. `10 of 30`) during chunked uploads.
+impact: Upload UI now shows progressive message in `X of Y` format while upload is in flight, improving clarity for large batch uploads.
+status: completed
+
+Summary:
+
+- Added `totalUploadFiles` state in both upload UIs (new appeal + representations).
+- In-progress upload message now renders as:
+ - `${uploadCountMessage} of ${totalUploadFiles}`
+- Upload start now initializes as `0 of Y` and updates after each completed chunk callback.
+- Empty-drop/reset path clears both counters to avoid stale totals.
+
+Validation:
+
+- `npx eslint actions/services/documentDirectService.js components/elements/index.js components/case/representation/representationElements.js` -> executed with no lint output.
+
+Follow-ups:
+
+- Optional: localize a dedicated `upload-progress-x-of-y` translation key if copy needs stronger grammatical control per locale.