From 737b74539d7daee0a9a9fee0f46f1d9cda6e26a2 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 20 Jan 2026 15:20:01 +0000 Subject: [PATCH 1/2] 21154 updated language labels --- locales/cy/myrepresentations.json | 1 + locales/cy/newappeal.json | 1 + locales/en/myrepresentations.json | 1 + locales/en/newappeal.json | 1 + 4 files changed, 4 insertions(+) diff --git a/locales/cy/myrepresentations.json b/locales/cy/myrepresentations.json index c776853d..e39e7422 100644 --- a/locales/cy/myrepresentations.json +++ b/locales/cy/myrepresentations.json @@ -35,6 +35,7 @@ "fileupload-file-error-filesize-label": "yn rhy fawr. Llwythwch ffeil lai i fyny.", "fileupload-file-error-invalid-type-label": "mae ganddo fath annilys. Uwchlwythwch fath a ganiateir.", "fileupload-file-error-invalid-label": "yn annilys", + "fileupload-file-error-invalid-filename-label": "mae ganddo enw ffeil annilys", "capacity-para-one": "Mae'r ffurflen hon yn eich galluogi i gyflwyno sylwadau ar achos i Benderfyniadau Cynllunio ac Amgylchedd Cymru", "capacity-para-two": "Sylwch fod angen cyflwyno sylwadau gan bartïon â diddordeb o fewn yr amserlen. Mae hwn i'w weld ar y dudalen \"Crynodeb Achos\" flaenorol. Gall sylwadau a gyflwynir ar ôl y dyddiad hwn gael eu hystyried yn annilys.", "sips-capacity-para-one": "Mae'r ffurflen hon yn eich galluogi i gyflwyno ymateb i ymgynghoriad ar achos i Benderfyniadau Cynllunio ac Amgylchedd Cymru.", diff --git a/locales/cy/newappeal.json b/locales/cy/newappeal.json index 4ba06f38..b3e69546 100644 --- a/locales/cy/newappeal.json +++ b/locales/cy/newappeal.json @@ -86,6 +86,7 @@ "new-appeal-fileupload-file-error-filesize-label": "yn rhy fawr. Llwythwch ffeil lai i fyny.", "new-appeal-fileupload-file-error-invalid-type-label": "mae ganddo fath annilys. Uwchlwythwch fath a ganiateir.", "new-appeal-fileupload-file-error-invalid-label": "yn annilys", + "new-appeal-fileupload-file-error-invalid-filename-label": "mae ganddo enw ffeil annilys", "s78-appeals-only-label": "Gallwch ddefnyddio'r porth hwn i gyflwyno apêl yn erbyn y canlynol yn unig:", "s78-appeals-only-bullet-one": "gwrthod caniatâd cynllunio", "s78-appeals-only-bullet-two": "methiant i benderfynu ar gais cynllunio o fewn 8 wythnos", diff --git a/locales/en/myrepresentations.json b/locales/en/myrepresentations.json index 8618b50f..2ede7c8f 100644 --- a/locales/en/myrepresentations.json +++ b/locales/en/myrepresentations.json @@ -35,6 +35,7 @@ "fileupload-file-error-filesize-label": "is too large. Please upload a smaller file.", "fileupload-file-error-invalid-type-label": "has an invalid type. Please upload an allowed type.", "fileupload-file-error-invalid-label": "is invalid", + "fileupload-file-error-invalid-filename-label": "has invalid filename", "capacity-para-one": "This form enables you to submit representations on a case to Planning and Environment Decisions Wales.", "capacity-para-two": "Please note that representations from interested parties need to be made within the timetable. This can be found on the previous \"Case Summary\" page. Representations submitted after this date may be considered invalid.", "sips-capacity-para-one": "This form enables you to submit a consultation repsonse on a case to Planning and Environment Decisions Wales.", diff --git a/locales/en/newappeal.json b/locales/en/newappeal.json index 6414ee36..f6e31eeb 100644 --- a/locales/en/newappeal.json +++ b/locales/en/newappeal.json @@ -86,6 +86,7 @@ "new-appeal-fileupload-file-error-filesize-label": "is too large. Please upload a smaller file.", "new-appeal-fileupload-file-error-invalid-type-label": "has an invalid type. Please upload an allowed type.", "new-appeal-fileupload-file-error-invalid-label": "is invalid", + "new-appeal-fileupload-file-error-invalid-filename-label": "has invalid filename", "s78-appeals-only-label": "You can only use this portal to submit an appeal against:", "s78-appeals-only-bullet-one": "a refusal of planning permission", "s78-appeals-only-bullet-two": "failure to determine a planning application within 8 weeks", From e6b0b0763ad6302274c0b08a4e4b437770479ec0 Mon Sep 17 00:00:00 2001 From: robbond Date: Tue, 20 Jan 2026 15:20:54 +0000 Subject: [PATCH 2/2] 21099 update filename validation with # --- .../representation/representationElements.js | 84 +++++++- components/elements/index.js | 194 ++++++++++++------ components/newappeal/buildrow.js | 2 + pages/myportal/representation.js | 3 +- 4 files changed, 218 insertions(+), 65 deletions(-) diff --git a/components/case/representation/representationElements.js b/components/case/representation/representationElements.js index 65c38700..db0675b3 100644 --- a/components/case/representation/representationElements.js +++ b/components/case/representation/representationElements.js @@ -1042,7 +1042,9 @@ export const RenderFileUpload = (field) => { }); }; + const [errorMessage, setErrorMessage] = useState(null); const [rejectedFiles, setRejectedFiles] = useState([]); // Store rejected files + const [completedUploadFiles, setCompletedUploadFiles] = useState(false); const [uploadCountMessage, setUploadCountMessage] = useState(""); @@ -1060,6 +1062,15 @@ export const RenderFileUpload = (field) => { "myrepresentations: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( "myrepresentations:fileupload-file-error-invalid-label" )}`; @@ -1076,6 +1087,44 @@ export const RenderFileUpload = (field) => { setRejectedFiles([]); // Clear any previously rejected file errorsss }; + const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-()]+$/; + + const validateFilename = (file, t) => { + const name = file.name; + + // block # + if (name.includes("#")) { + return { + code: "filename-invalid-chars", + message: `${name} ${t( + "myrepresentations:fileupload-file-error-invalid-filename-label" + )}`, + }; + } + + // broader blocklist (optional but recommended) + if (!FILENAME_ALLOWED.test(name)) { + return { + code: "filename-invalid-chars", + message: `${name} ${t( + "myrepresentations:fileupload-file-error-invalid-filename-label" + )}`, + }; + } + + // block characters < > : " / \ | ? * + if (/[<>:"/\\|?*]/.test(name)) { + return { + code: "filename-invalid-chars", + message: `${name} ${t( + "myrepresentations:fileupload-file-error-invalid-filename-label" + )}`, + }; + } + + return null; // valid + }; + return ( <> { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], }} - onDrop={(filesToUpload, e) => { + validator={(file) => validateFilename(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); + return; + } + setCompletedUploadFiles(false); - const renamedAcceptedFiles = filesToUpload.map( + + const renamedAcceptedFiles = acceptedFiles.map( (file) => new File( [file], @@ -1115,7 +1182,16 @@ export const RenderFileUpload = (field) => { } ) ); + field.input.onChange(renamedAcceptedFiles); + + if (typeof field.setFileCount === "function") { + field.setFileCount( + (field.uploadCount || 0) + + renamedAcceptedFiles.length + ); + } + setUploadCountMessage(renamedAcceptedFiles.length); uploadSingleFile( @@ -1196,8 +1272,6 @@ export const RenderFileUpload = (field) => { }); }); }} - onDropRejected={handleDropRejected} - onDropAccepted={handleDropAccepted} > {({ getRootProps, getInputProps }) => ( <> @@ -1224,7 +1298,7 @@ export const RenderFileUpload = (field) => { )} - {rejectedFiles.length > 0 && completedUploadFiles == true && ( + {rejectedFiles.length > 0 && (
{ }; export function FileUploadField(props) { + const uploadCount = + typeof props.uploadCount === "number" ? props.uploadCount : 0; + const setFileCount = + typeof props.setFileCount === "function" + ? props.setFileCount + : () => {}; + return ( <>

@@ -2144,6 +2151,14 @@ const RenderFileUpload = (field) => { "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" )}`; @@ -2160,6 +2175,44 @@ const RenderFileUpload = (field) => { setRejectedFiles([]); // Clear any previously rejected file errorsss }; + const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-()]+$/; + + const validateFilename = (file, t) => { + const name = file.name; + + // block # + if (name.includes("#")) { + return { + code: "filename-invalid-chars", + message: `${name} ${t( + "newappeal:new-appeal-fileupload-file-error-invalid-filename-label" + )}`, + }; + } + + // broader blocklist (optional but recommended) + if (!FILENAME_ALLOWED.test(name)) { + return { + code: "filename-invalid-chars", + message: `${name} ${t( + "newappeal:new-appeal-fileupload-file-error-invalid-filename-label" + )}`, + }; + } + + // block characters < > : " / \ | ? * + if (/[<>:"/\\|?*]/.test(name)) { + return { + code: "filename-invalid-chars", + message: `${name} ${t( + "newappeal:new-appeal-fileupload-file-error-invalid-filename-label" + )}`, + }; + } + + return null; // valid + }; + return ( <> { "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": [".xlsx"], }} - onDrop={(filesToUpload, e) => { + validator={(file) => validateFilename(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); + return; + } + + // continue with your existing upload logic for accepted files setCompletedUploadFiles(false); - const renamedAcceptedFiles = filesToUpload.map( + + const renamedAcceptedFiles = acceptedFiles.map( (file) => new File( [file], `${getDocumentType(field.documentTypeCode)}_-_${ file.name }`, - { - type: file.type, - } + { type: file.type } ) ); + field.input.onChange(renamedAcceptedFiles); - field.setFileCount( - field.uploadCount + renamedAcceptedFiles.length - ); + if (typeof field.setFileCount === "function") { + field.setFileCount( + (field.uploadCount || 0) + + renamedAcceptedFiles.length + ); + } setUploadCountMessage(renamedAcceptedFiles.length); @@ -2202,66 +2276,68 @@ const RenderFileUpload = (field) => { renamedAcceptedFiles, field.containerID, field.ticketnumber - ).then((data) => { - console.log(data); - let buildAppealFilesArray = []; - let values = field.form.appealForm.values; - let filesUploadObj = renamedAcceptedFiles; + ) + .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, + 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; }); - } + } - //console.log(buildAppealFilesArray); - function removeDuplicates(arr, key) { - const seen = new Set(); - return arr.filter((item) => { - const value = item[key]; - if (seen.has(value)) { - return false; // Duplicate found - } else { - seen.add(value); // Add value to the set - return true; // Keep the item - } + buildAppealFilesArray = values.hasOwnProperty( + "filesList" + ) + ? buildAppealFilesArray.concat(values.filesList) + : buildAppealFilesArray; + + buildAppealFilesArray = removeDuplicates( + buildAppealFilesArray, + "name" + ); + + Object.assign(values, { + filesList: buildAppealFilesArray, }); - } - buildAppealFilesArray = values.hasOwnProperty( - "filesList" - ) - ? buildAppealFilesArray.concat(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 + ); + } - buildAppealFilesArray = removeDuplicates( - buildAppealFilesArray, - "name" - ); - - Object.assign(values, { - "filesList": buildAppealFilesArray, - }); - data.invalidFiles.length > 0 && - (setRejectedFiles(data.invalidFiles), - setUploadCountMessage( - renamedAcceptedFiles.length - - data.invalidFiles.length - )); - getFilesFromBlobproxy( - field.containerID, - field.ticketnumber - ).then((data) => { - field.input.value == null; + return getFilesFromBlobproxy( + field.containerID, + field.ticketnumber + ); + }) + .then((data) => { field.setFilesForAppeal(data); setCompletedUploadFiles(true); }); - }); }} - onDropRejected={handleDropRejected} - onDropAccepted={handleDropAccepted} > {({ getRootProps, getInputProps }) => ( <> @@ -2288,7 +2364,7 @@ const RenderFileUpload = (field) => { )} - {rejectedFiles.length > 0 && completedUploadFiles == true && ( + {rejectedFiles.length > 0 && (
); // } else { diff --git a/pages/myportal/representation.js b/pages/myportal/representation.js index f48fbcea..d344e0a8 100644 --- a/pages/myportal/representation.js +++ b/pages/myportal/representation.js @@ -304,6 +304,7 @@ export const getServerSideProps = wrapper.getServerSideProps( "incidentid": result.incidentID, "appealType": result.appealType, "repDetails": result, + "filesList": result.filesList, }) ); @@ -318,7 +319,7 @@ export const getServerSideProps = wrapper.getServerSideProps( const repsFileListObj = await getRepsFilesBlobs( result.containerID, - result.ticketnumber, + result.ticketnumber || result.caseRef, result.repfile_name );