Merged PR 2228: refactoring fix and fix for language dependant login and download questionnaire

Related work items: #22303, #22344, #22500
This commit is contained in:
Robert Bond
2026-04-08 11:20:59 +00:00
32 changed files with 2918 additions and 2308 deletions
@@ -230,6 +230,7 @@ export const generateRepPDF = async (
var queryUrl =
"/api/file/generatepdf" + (options.download ? "?download=true" : "");
formValues.containerID = containerID;
try {
return await postSignedFileJson(queryUrl, formValues, {
...(options.download ? { responseType: "blob" } : {})
@@ -40,8 +40,7 @@ const PersonalDetailsComplete = (props) => {
console.log("=============== pref lang", prefLang);
setCookie(null, "pedw_locale", userLang, {
path: "/",
maxAge: 60 * 60 * 24 * 365
path: "/"
});
console.log("Set cookie to:", userLang);
@@ -0,0 +1,26 @@
import React from "react";
import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { Field } from "redux-form";
import { RenderCheckBox } from "./renderCheckBox";
export function CheckBoxfield(props) {
const router = useRouter();
let { t } = useTranslation();
const fieldOptions =
router.locale == "cy" ? ["Ydw", "Na", "2323"] : ["Yes", "No", "23232"];
return (
<Field
name={props.name}
datafieldname={props.datafieldname}
label={props.label}
options={fieldOptions}
id={props.id}
className={props.className}
errorMsg={t("newappeal:select-an-option-label")}
component={RenderCheckBox}
/>
);
}
@@ -0,0 +1,99 @@
import React from "react";
import _ from "lodash";
import useTranslation from "next-translate/useTranslation";
import { Field } from "redux-form";
import { RenderDatePicker } from "./renderDatePicker";
import { validateField } from "../validationUtils";
const dateDiffInDays = (a, b) => {
const _MS_PER_DAY = 1000 * 60 * 60 * 24;
const utc1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
const utc2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
return Math.floor((utc2 - utc1) / _MS_PER_DAY);
};
export function DateFieldPicker(props) {
const {
name,
label,
parentField,
form,
formProps,
parentFieldShowOnValue,
validation
} = props;
let dateStart = props.dateStart;
let dateEnd = props.dateEnd;
let { t } = useTranslation();
const showIfHasParentShowValue =
parentField != false &&
!_.isEmpty(formProps[form]) &&
_.has(formProps[form].values, parentField) &&
formProps[form].values[parentField] == parentFieldShowOnValue;
const requiredMessage = t("newappeal:is-required-label");
const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
dateStart =
showIfHasParentShowValue && name == "pinswg_dateoflpadecision"
? "-" +
(dateDiffInDays(
new Date(formProps[form].values["pinswg_dateofapplication"]),
new Date()
) -
1) +
"d"
: dateStart;
return (
<div className="govuk-form-group">
{parentField != false ? (
showIfHasParentShowValue && (
<Field
name={name}
id={name}
label={label}
component={RenderDatePicker}
validate={(value) =>
validateField(
value,
validation,
requiredMessage,
emojiNotAllowedMessage,
invalidPostcodeMessage
)
}
errorMsg={t("newappeal:select-a-date-label")}
dateStart={dateStart}
dateEnd={dateEnd}
hint={props.hint}
/>
)
) : (
<Field
name={name}
id={name}
label={label}
component={RenderDatePicker}
validate={(value) =>
validateField(
value,
validation,
requiredMessage,
emojiNotAllowedMessage,
invalidPostcodeMessage
)
}
errorMsg={t("newappeal:select-a-date-label")}
dateStart={dateStart}
dateEnd={dateEnd}
hint={props.hint}
/>
)}
</div>
);
}
+106
View File
@@ -0,0 +1,106 @@
import React from "react";
import _ from "lodash";
import useTranslation from "next-translate/useTranslation";
import { Field } from "redux-form";
import { RenderTextfield } from "./renderTextfield";
import { RenderDecimalField } from "./renderDecimalField";
import { validateField } from "../validationUtils";
const isVisibleByInclusion = ({
parentField,
form,
formProps,
parentFieldShowOnValue
}) =>
parentField != false &&
!_.isEmpty(formProps[form]) &&
_.has(formProps[form].values, parentField) &&
parentFieldShowOnValue.indexOf(
formProps[form].values[parentField].toString()
) > -1;
export function DecimalField(props) {
const {
name,
validation,
form,
formProps,
parentFieldShowOnValue,
parentField
} = props;
let { t } = useTranslation();
const showIfHasParentShowValue = isVisibleByInclusion({
parentField,
form,
formProps,
parentFieldShowOnValue
});
const validateDecimal = (value) => {
if (!value) return t("newappeal:is-required-label");
const regex = /^\d{1,6}(\.\d{1,2})?$/;
if (!regex.test(value)) {
return t("newappeal:invalid-decimal-label");
}
return undefined;
};
const requiredMessage = t("newappeal:is-required-label");
const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
return (
<>
{parentField != false ? (
showIfHasParentShowValue && (
<div className="govuk-form-group">
<Field
name={props.name}
id={props.name}
type="number"
className="govuk-input govuk-input--width-10"
spellCheck="false"
aria-describedby={name}
pattern="[0-9]*"
inputMode="numeric"
parse={Number}
validate={(value) =>
validateField(
value,
validation,
requiredMessage,
emojiNotAllowedMessage,
invalidPostcodeMessage
)
}
component={RenderTextfield}
label={props.label}
maxFieldLength={props.maxFieldLength}
/>
</div>
)
) : (
<div className="govuk-form-group">
<Field
name={props.name}
id={props.name}
type="text"
className="govuk-input govuk-input--width-10"
spellCheck="false"
aria-describedby={name}
pattern="^\d*\.?\d+$"
validate={[validateDecimal]}
component={RenderDecimalField}
label={props.label}
maxFieldLength={props.maxFieldLength}
/>
</div>
)}
</>
);
}
@@ -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 && (
<FieldArray
name={name}
component={RenderSubFields}
maxSubField={maxSubField}
/>
)}
</>
);
};
+114
View File
@@ -0,0 +1,114 @@
import React from "react";
import _ from "lodash";
import useTranslation from "next-translate/useTranslation";
import { Field } from "redux-form";
import { RenderTextfield } from "./renderTextfield";
import { validateField } from "../validationUtils";
const isVisibleByInclusion = ({
parentField,
form,
formProps,
parentFieldShowOnValue
}) =>
parentField != false &&
!_.isEmpty(formProps[form]) &&
_.has(formProps[form].values, parentField) &&
parentFieldShowOnValue.indexOf(
formProps[form].values[parentField].toString()
) > -1;
export function NumericField(props) {
const {
name,
label,
validation,
form,
formProps,
parentFieldShowOnValue,
parentField
} = props;
let { t } = useTranslation();
const required = (value) => {
return value || value == 0
? undefined
: t("newappeal:is-required-label");
};
const isNumber = (value) => {
const regex = /^\d+$/;
return regex.test(value)
? undefined
: t("newappeal:invalid-number-label");
};
const maxLength = (max) => (value) =>
value && value.length > max
? `Must be ${max} characters or less`
: undefined;
const showIfHasParentShowValue = isVisibleByInclusion({
parentField,
form,
formProps,
parentFieldShowOnValue
});
const requiredMessage = t("newappeal:is-required-label");
const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
return (
<>
{parentField != false ? (
showIfHasParentShowValue && (
<div className="govuk-form-group">
<Field
name={props.name}
id={props.name}
type="number"
className="govuk-input govuk-input--width-20"
spellCheck="false"
aria-describedby={name}
pattern="[0-9]*"
inputMode="numeric"
parse={Number}
validate={(value) =>
validateField(
value,
validation,
requiredMessage,
emojiNotAllowedMessage,
invalidPostcodeMessage
)
}
component={RenderTextfield}
label={label}
maxFieldLength={props.maxFieldLength}
/>
</div>
)
) : (
<div className="govuk-form-group">
<Field
name={props.name}
id={props.name}
type="text"
className="govuk-input govuk-input--width-10"
aria-describedby={name}
pattern="[0-9]*"
validate={[
required,
isNumber,
maxLength(props.maxFieldLength)
]}
component={RenderTextfield}
label={props.label}
maxFieldLength={props.maxFieldLength}
/>
</div>
)}
</>
);
}
@@ -0,0 +1,29 @@
import React from "react";
import useTranslation from "next-translate/useTranslation";
import { Field } from "redux-form";
import { RenderPickList } from "./renderPickList";
export function PickList(props) {
const { datafieldname } = props;
let { t } = useTranslation();
const required = (value) => {
return value || value == 0
? undefined
: t("newappeal:is-required-label");
};
return (
<Field
name={props.name}
id={props.name}
label={props.label}
component={RenderPickList}
datafieldname={datafieldname}
picklistData={props.picklistData}
hint={props.hint}
errorMsg={t("newappeal:is-required-label")}
validate={[required]}
/>
);
}
+107
View File
@@ -0,0 +1,107 @@
import React from "react";
import _ from "lodash";
import useTranslation from "next-translate/useTranslation";
import { Field } from "redux-form";
import { RenderRadio } from "./renderRadio";
import { validateField } from "../validationUtils";
const isVisibleByInclusion = ({
parentField,
form,
formProps,
parentFieldShowOnValue
}) =>
parentField != false &&
!_.isEmpty(formProps[form]) &&
_.has(formProps[form].values, parentField) &&
parentFieldShowOnValue.indexOf(
formProps[form].values[parentField].toString()
) > -1;
export function Radiofield(props) {
const {
name,
label,
datafieldname,
form,
formProps,
parentFieldShowOnValue,
parentField,
validation
} = props;
let { t } = useTranslation();
const fieldOptions = props.options
.slice(1, props.options.length - 1)
.split(",");
const showIfHasParentShowValue = isVisibleByInclusion({
parentField,
form,
formProps,
parentFieldShowOnValue
});
const requiredMessage = t("newappeal:is-required-label");
const emojiNotAllowedMessage = t("newappeal:emojis-not-allowed-label");
const invalidPostcodeMessage = t("newappeal:invalid-postcode-label");
return (
<>
{parentField != false ? (
showIfHasParentShowValue && (
<Field
name={name}
datafieldname={datafieldname}
label={label}
options={fieldOptions}
id={name}
errorMsg={t("newappeal:select-an-option-label")}
component={RenderRadio}
inline={props.inline}
hint={props.hint}
validate={(value) =>
validateField(
value,
validation,
requiredMessage,
emojiNotAllowedMessage,
invalidPostcodeMessage
)
}
requiredDocumentLabel={props.requiredDocumentLabel}
requiredDocumentValue={props.requiredDocumentValue}
setDocumentsList={props.setDocumentsList}
documentList={props.documentList}
/>
)
) : (
<Field
name={name}
datafieldname={datafieldname}
label={label}
options={fieldOptions}
id={name}
errorMsg={t("newappeal:select-an-option-label")}
component={RenderRadio}
inline={props.inline}
hint={props.hint}
validate={(value) =>
validateField(
value,
validation,
requiredMessage,
emojiNotAllowedMessage,
invalidPostcodeMessage
)
}
requiredDocumentLabel={props.requiredDocumentLabel}
requiredDocumentValue={props.requiredDocumentValue}
setDocumentsList={props.setDocumentsList}
documentList={props.documentList}
/>
)}
</>
);
}
@@ -0,0 +1,89 @@
import React from "react";
import { Field } from "redux-form";
import { useRouter } from "next/router";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import { getFieldTranslation } from "../helpers/translationHelpers";
export const RenderCheckBox = ({
datafieldname,
name,
label,
id,
errorMsg,
options,
input: { onChange, value },
meta: { touched, error },
...custom
}) => {
const router = useRouter();
const { locale } = router;
const { appealtypes } = router.query;
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<fieldset
className="govuk-fieldset"
aria-describedby={name + "-hint"}
>
<legend className="govuk-fieldset__legend govuk-fieldset__legend--s govuk-!-font-weight-regular">
<label
className="govuk-fieldset__heading govuk-body-m"
id={id + "_label"}
>
{translatedLabel}
</label>
</legend>
{touched && error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{errorMsg}
</span>
)}
<div className="govuk-checkboxes ">
{Object.keys(options).map((key, index) => (
<div
className="govuk-checkboxes__item"
data-children-count={key}
key={key}
>
<Field
id={id + "_" + index}
name={id + "_" + index}
component="input"
type="checkbox"
className="govuk-checkboxes__input"
value={options[key]}
/>
<label
className="govuk-label govuk-checkboxes__label"
htmlFor={id + "_" + index}
>
{options[key]}
</label>
</div>
))}
</div>
</fieldset>
</div>
</>
);
};
@@ -0,0 +1,162 @@
import React from "react";
import DatePicker from "react-datepicker";
import { useRouter } from "next/router";
import useTranslation from "next-translate/useTranslation";
import {
addDays,
addMonths,
addYears,
parseISO,
subDays,
subMonths,
subYears
} from "date-fns";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import { getFieldTranslation } from "../helpers/translationHelpers";
export const RenderDatePicker = ({
datafieldname,
name,
label,
id,
errorMsg,
input: { onChange, value },
meta: { touched, error },
...custom
}) => {
const router = useRouter();
let { t } = useTranslation();
const { locale } = router;
const { appealtypes } = router.query;
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
function dateFromOffset(offsetStr) {
const today = new Date();
if (!offsetStr || offsetStr === "0") {
return today;
}
const match = offsetStr.match(/^([+-])(\d+)([dmy])$/i);
if (!match) {
console.warn("Invalid offset format:", offsetStr);
return today;
}
const [, sign, numberStr, unitRaw] = match;
const amount = parseInt(numberStr, 10);
const unit = unitRaw.toLowerCase();
const isNegative = sign === "-";
switch (unit) {
case "y":
return isNegative
? subYears(today, amount)
: addYears(today, amount);
case "m":
return isNegative
? subMonths(today, amount)
: addMonths(today, amount);
case "d":
return isNegative
? subDays(today, amount)
: addDays(today, amount);
default:
return today;
}
}
const minDate = dateFromOffset(custom.dateStart);
const maxDate = dateFromOffset(custom.dateEnd);
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<fieldset
className="govuk-fieldset"
role="group"
aria-describedby={id + "_label"}
>
<legend className="govuk-fieldset__legend govuk-fieldset__legend--s govuk-!-font-weight-regular">
<label
className="govuk-fieldset__heading"
id={id + "_label"}
htmlFor={id}
>
{translatedLabel}
</label>
{custom.hasOwnProperty("hint") && (
<div className="govuk-hint govuk-!-font-size-14">
{t(custom.hint)}
</div>
)}
</legend>
{!custom.hasOwnProperty("showErrorBottom")
? touched &&
error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{errorMsg}
</span>
)
: ""}
<DatePicker
{...custom}
locale={router.locale}
autoOk={true}
id={id}
dateFormat="dd/MM/yyyy"
onChange={onChange}
selected={
value
? typeof value != "object"
? parseISO(value)
: value
: null
}
className="govuk-input govuk-input--width-10"
minDate={minDate}
maxDate={maxDate}
autoComplete="off"
/>
{custom.hasOwnProperty("showErrorBottom")
? touched &&
error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{error}
</span>
)
: ""}
</fieldset>
</div>
</>
);
};
@@ -0,0 +1,62 @@
import React from "react";
import { useRouter } from "next/router";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import { getFieldTranslation } from "../helpers/translationHelpers";
export const RenderDecimalField = ({
name,
id,
label,
datafieldname,
className,
input,
errorMsg,
meta: { touched, error },
...custom
}) => {
const router = useRouter();
const { locale } = router;
const { appealtypes } = router.query;
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
{translatedLabel}
</label>
{touched && error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{error}
</span>
)}
<input
{...input}
className={className}
name={name}
id={id}
maxLength={
custom.hasOwnProperty("maxFieldLength")
? custom.maxFieldLength
? custom.maxFieldLength
: 100
: 200
}
/>
</div>
</>
);
};
@@ -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) => (
<div key={file.name} className="govuk-!-margin-bottom-5 fileThumb">
<img src={file.preview} alt={file.name} width="50" /> -{" "}
<span className="govuk-body">
{file.name} ({file.size / 1024}kb)
</span>
</div>
));
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 (
<>
<Dropzone
key={field.input.name + "_key"}
maxSize={50 * 1024 * 1024}
accept={{
"application/pdf": [".pdf"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document":
[".docx"],
"image/tiff": [".tif", ".tiff"],
"image/jpeg": [".jpg", ".jpeg"],
"image/png": [".png"],
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
[".xlsx"]
}}
validator={(file) => 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 }) => (
<>
<section className="container">
<div {...getRootProps({ style })}>
{" "}
<input id={field.id} {...getInputProps()} />
<div className="govuk-!-font-size-14">
{t(
"newappeal:new-appeal-fileupload-drop-label"
)}{" "}
(Max 50 MB)
</div>
<em className="govuk-!-font-size-14">
(
{t(
"newappeal:new-appeal-fileupload-file-list-label"
)}
)
</em>
</div>
{/* <aside>{thumbs}</aside> */}
</section>
</>
)}
</Dropzone>
{rejectedFiles.length > 0 && (
<div
style={{ color: "red", marginTop: "10px" }}
className="govuk-body govuk-!-font-size-14 "
>
<ul>
{rejectedFiles.map((error, index) => (
<li key={index}>{error}</li>
))}
</ul>
</div>
)}
{totalUploadFiles > 0 && completedUploadFiles == false && (
<div className="govuk-body govuk-!-font-size-14">
{t("home:uploading-files-label", {
number:
totalUploadFiles > 0
? `${uploadCountMessage} of ${totalUploadFiles}`
: uploadCountMessage
})}
</div>
)}
{completedUploadFiles > 0 && uploadCountMessage >= 0 && (
<div className="govuk-body govuk-!-font-size-14">
{t("home:completed-uploading-files-label", {
number: uploadCountMessage
})}
</div>
)}
{field.meta.touched && field.meta.error && (
<span className="error">{field.meta.error}</span>
)}
{files && Array.isArray(files) && (
<>
{/* {(files = removeEmptyObjects(files))} */}
{!completedUploadFiles &&
files.map(
(file, i) =>
typeof file.name != "undefined" && (
<div
key={file.name}
className="govuk-!-margin-bottom-5 fileThumb"
>
{" "}
{/* <span
className="govuk-summary-list__actionLink watched_link govuk-!-margin-right-5 govuk-!-text-align-left"
onClick={() => removeFile(file)}
>
&minus;
</span> */}
<img
src={getThumbnailIconByMimeType(
file.type
)}
alt={file.name}
width="50"
/>
<span className="govuk-body govuk-!-font-size-14 govuk-!-padding-left-5">
{file.name} (
{bytesToSize(file.size)}) <br />
<div id={file.name + "_" + i}>
{completedUploadFiles ? (
<p className="govuk-body govuk-!-font-size-14">
Upload complete
</p>
) : (
<p className="govuk-body textloading govuk-!-font-size-14">
{t(
"home:uploading-files-progress-label"
)}{" "}
</p>
)}
</div>
</span>
</div>
)
)}
</>
)}
{blobList.length > 0 && (
<h4>{t("newappeal:previously-added-files")}</h4>
)}
{blobList.map(
(blob, i) =>
blob.documentType == field.documentTypeCode && (
<div
key={blob.name + "_" + i}
className="govuk-!-margin-bottom-5 fileThumb govuk-!-margin-top-5"
>
<div key={"deleteLnk" + i} className="">
<span
className="govuk-summary-list__actionLink watched_link govuk-!-margin-right-5 govuk-!-text-align-left"
onClick={() => {
setUploadCountMessage(
uploadCountMessage - 1
);
deleteThisBlob(
field.containerID,
blob.name,
blob.hasheddeletepath,
blob.hashgetblobs,
field.ticketnumber
);
}}
title={t("home:remove-this-file-label")}
>
&minus;
</span>
</div>
<img
src={getThumbnailIconByExtension(blob.name)}
alt={blob.name}
width="50"
/>
<Link
key={"lnk" + i}
scroll={false}
href={
"/api/file/downloadblob?container=" +
field.containerID +
"&casefolderID=" +
encodeURIComponent(field.ticketnumber) +
"&blobname=" +
encodeURIComponent(blob.name) +
blob.hashedfilepath
}
className="govuk-body govuk-!-font-size-14 govuk-!-padding-left-5 govuk-link"
>
{blob.name} ({bytesToSize(blob.contentLength)})
</Link>
</div>
)
)}{" "}
</>
);
};
@@ -0,0 +1,51 @@
import React from "react";
export const RenderMultiline = ({
id,
className,
rows,
datafieldname,
name,
label,
input,
errorMsg,
meta: { touched, error },
...custom
}) => {
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
{touched && error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{touched &&
((error && (
<span>{errorMsg ? errorMsg : error}</span>
)) ||
(warning && <span>{warning}</span>))}
</span>
)}
<textarea
id={id}
name={id}
rows={rows}
className={className}
{...input}
maxLength={
custom.hasOwnProperty("maxFieldLength")
? custom.maxFieldLength
? custom.maxFieldLength
: 800
: 800
}
></textarea>
</div>
</>
);
};
@@ -0,0 +1,82 @@
import React from "react";
import { JSONPath as jsonpath } from "jsonpath-plus";
import { useRouter } from "next/router";
import useTranslation from "next-translate/useTranslation";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import pickListLookup from "../../../data/picklistLookups.json";
import {
getFieldTranslation,
getPickListTranslation
} from "../helpers/translationHelpers";
export const RenderPickList = ({
datafieldname,
picklistData,
id,
name,
label,
input,
errorMsg,
meta: { touched, error }
}) => {
let { t } = useTranslation();
const router = useRouter();
const { locale } = router;
const { appealtypes } = router.query;
var dropdownObj = jsonpath({
path:
"$..value[?(@ && @.LogicalName=='" + datafieldname + "')]..Options",
json: picklistData,
eval: true
});
dropdownObj = dropdownObj[0];
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
{translatedLabel}
</label>
{touched && error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{error}
</span>
)}
<select {...input} className="govuk-select " id={id} name={id}>
<option value="" disabled defaultValue>
{t("newappeal:select-default")}
</option>
{Object.keys(dropdownObj).map((key, index) => {
const translatedOption = getPickListTranslation({
optionValue:
dropdownObj[key].Label.LocalizedLabels[0].Label,
locale,
pickListLookup
});
return (
<option key={key} value={dropdownObj[key].Value}>
{translatedOption}
</option>
);
})}
</select>
</div>
</>
);
};
+129
View File
@@ -0,0 +1,129 @@
import React from "react";
import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { Field } from "redux-form";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import { getFieldTranslation } from "../helpers/translationHelpers";
export const RenderRadio = ({
datafieldname,
name,
label,
id,
errorMsg,
options,
input: { onChange, value },
meta: { touched, error },
...custom
}) => {
let { t } = useTranslation();
const router = useRouter();
const { locale } = router;
const { appealtypes } = router.query;
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<fieldset
className="govuk-fieldset"
aria-describedby={id + "_label"}
>
<legend className="govuk-fieldset__legend govuk-fieldset__legend--s govuk-!-font-weight-regular">
<label
className="govuk-fieldset__heading govuk-body-m"
id={id + "_label"}
>
{translatedLabel}
</label>
</legend>
{touched && error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{errorMsg}
</span>
)}
<div
className={
custom.inline == false
? "govuk-radios govuk-radios--small"
: "govuk-radios govuk-radios--inline govuk-radios--small"
}
>
{" "}
{custom.hint != false && (
<div className="govuk-hint">{t(custom.hint)}</div>
)}
{Object.keys(options).map((key, index) => (
<div
className="govuk-radios__item"
data-children-count={key}
key={key}
>
<Field
id={id + "_" + index}
name={id}
component="input"
type="radio"
className="govuk-radios__input"
value={options[key].split("|")[0]}
checked={
typeof value != "undefined" &&
value ==
parseInt(options[key].split("|")[0])
? true
: false
}
onChange={(e) => {
let docListObj = custom.documentList;
if (
custom.requiredDocumentValue ==
"Yes"
) {
e.target.value != "846040000"
? (docListObj = Object.assign(
docListObj,
{
["documentCheckList_" +
id]:
custom.requiredDocumentLabel
}
))
: delete docListObj[
"documentCheckList_" + id
];
custom.setDocumentsList(docListObj);
}
}}
/>
<label
className="govuk-label govuk-radios__label"
htmlFor={id + "_" + index}
>
{t(options[key].split("|")[1])}
</label>
</div>
))}
</div>
</fieldset>
</div>
</>
);
};
@@ -0,0 +1,81 @@
import React, { useState } from "react";
import dynamic from "next/dynamic";
import { updateLinks } from "../../utils";
const ReactQuill = dynamic(() => import("react-quill-new"), { ssr: false });
export const RenderRichMultiline = ({
id,
className,
rows,
datafieldname,
name,
label,
input,
meta: { touched, error },
...custom
}) => {
const [editValue, setEditValue] = useState(
input.value != null ? input.value : ""
);
const changeEdit = (valStr) => {
setEditValue(valStr);
input.onChange(valStr);
};
const handleBlur = () => {
const updated = updateLinks(editValue);
setEditValue(updated);
input.onChange(updated);
};
var modules = {
toolbar: [
[{ "header": [1, 2, false] }],
["bold", "italic", "underline", "blockquote"],
[{ "list": "ordered" }, { "list": "bullet" }],
["clean"]
]
};
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<label className="govuk-label" htmlFor={name}>
{label}
</label>
{error && (
<span id={id + "-error"} className="govuk-error-message">
<span className="govuk-visually-hidden">Error:</span>{" "}
{error}
</span>
)}
<ReactQuill
modules={modules}
theme="snow"
value={editValue}
onChange={changeEdit}
onBlur={handleBlur}
/>
<textarea
{...input}
id={id}
name={name}
rows={rows}
className={className}
value={editValue}
style={{ display: "none" }}
></textarea>
</div>
</>
);
};
@@ -0,0 +1,150 @@
import React, { useEffect } from "react";
import { Field } from "redux-form";
import useTranslation from "next-translate/useTranslation";
import { RenderTextfield } from "./renderTextfield";
import { RenderDatePicker } from "./renderDatePicker";
export const RenderSubFields = ({
fields,
meta: { touched, error },
...custom
}) => {
let { t } = useTranslation();
let subTitle = fields.name.split("pinswg_")[1];
const checkValue = (value) => {
let errors;
if (!value) {
errors = t("newappeal:is-required-label");
} else {
const emojiRegex =
/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F700}-\u{1F77F}\u{1F780}-\u{1F7FF}\u{1F800}-\u{1F8FF}\u{1F900}-\u{1F9FF}\u{1FA00}-\u{1FA6F}\u{1FA70}-\u{1FAFF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}\u{2300}-\u{23FF}\u{2B50}\u{1F004}-\u{1F0CF}\u{1F0A0}-\u{1F0A5}\u{1F170}-\u{1F251}]/gu;
if (emojiRegex.test(value)) {
errors = "Emojis are not allowed";
}
}
return errors;
};
const required = (value) =>
value ? undefined : t("newappeal:is-required-label");
const email = (value) => {
let errors;
const emailRegex =
/(?:[a-z0-9!#$%&'*+\/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])/gi;
if (!emailRegex.test(value)) {
errors = t("newappeal:invalid-email-address-errormsg");
}
return errors;
};
useEffect(() => {
if (fields.length === 0) {
fields.push({});
}
}, [fields, fields.length]);
return (
<ul className="subFieldList">
{fields.map((member, index) => (
<div key={index}>
<h4>
{subTitle == "owners" ? "Owner" : "Tenant"} #{index + 1}
</h4>
<div className="fieldArrayContainer">
<div className="govuk-grid-column-full">
<div
className="gouk-grid-row"
style={{
display: "flex",
marginBottom: "20px"
}}
>
<Field
id={`${member}.${fields.name}_firstname`}
name={`${member}.${fields.name}_firstname`}
type="text"
component={RenderTextfield}
validate={[checkValue]}
label={"First name"}
className="govuk-input govuk-input--width-12"
showErrorBottom={true}
/>
<Field
id={`${member}.${fields.name}_lastname`}
name={`${member}.${fields.name}_lastname`}
type="text"
component={RenderTextfield}
validate={[checkValue]}
label={"Last name"}
className="govuk-input govuk-input--width-12"
showErrorBottom={true}
/>
</div>
<div
className="gouk-grid-row"
style={{ display: "flex" }}
>
<Field
id={`${member}.${fields.name}_email`}
name={`${member}.${fields.name}_email`}
type="text"
component={RenderTextfield}
validate={[email]}
label={"Email"}
className="govuk-input govuk-input--width-12"
showErrorBottom={true}
/>
<Field
name={`${member}.dateserved`}
type="text"
component={RenderDatePicker}
label="Date served "
dateStart={"-6m"}
dateEnd={"0"}
validate={[required]}
className="govuk-input govuk-input--width-12"
showErrorBottom={true}
/>
</div>
</div>
<div
className="gouk-grid-row"
style={{ display: "flex" }}
>
<button
type="button"
title="Remove"
onClick={() => fields.remove(index)}
className="govuk-button fieldArrayDelete"
>
X
</button>
</div>
</div>
</div>
))}
<br />
{fields.length < custom.maxSubField && (
<button
type="button"
className="govuk-link fieldArrayLink"
onClick={() => fields.push({})}
>
{subTitle == "owners"
? t("newappeal:new-appeal-add-owner-label")
: t("newappeal:new-appeal-add-tenant-label")}
</button>
)}
</ul>
);
};
@@ -0,0 +1,91 @@
import React from "react";
import { useRouter } from "next/router";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import { getFieldTranslation } from "../helpers/translationHelpers";
export const RenderTextfield = ({
id,
className,
rows,
datafieldname,
name,
label,
input,
errorMsg,
meta: { touched, error },
...custom
}) => {
const router = useRouter();
const { locale } = router;
const { appealtypes } = router.query;
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<label className="govuk-label " htmlFor={id} id={id + "_label"}>
{translatedLabel}
</label>
{!custom.hasOwnProperty("showErrorBottom")
? touched &&
error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{error}
</span>
)
: ""}
<input
{...input}
type={custom.type}
className={className}
name={name}
id={id}
maxLength={
custom.hasOwnProperty("maxFieldLength")
? custom.maxFieldLength
? custom.maxFieldLength
: 100
: 200
}
pattern={
custom.hasOwnProperty("pattern")
? custom.pattern
: undefined
}
/>
{custom.hasOwnProperty("showErrorBottom")
? touched &&
error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{error}
</span>
)
: ""}
</div>
</>
);
};
+135
View File
@@ -0,0 +1,135 @@
import React from "react";
import { useRouter } from "next/router";
import { Field } from "redux-form";
import fieldLookup from "../../../data/crmfieldlookuptranslations.json";
import { getFieldTranslation } from "../helpers/translationHelpers";
export const RenderYesNo = ({
datafieldname,
name,
label,
id,
errorMsg,
options,
input: { onChange, value },
meta: { touched, error },
...custom
}) => {
const router = useRouter();
const { locale } = router;
const { appealtypes } = router.query;
const translatedLabel = getFieldTranslation({
label,
locale,
appealtypes,
fieldLookup
});
return (
<>
<div
className={
touched && error
? "govuk-form-group govuk-form-group--error"
: "govuk-form-group "
}
>
<fieldset
className="govuk-fieldset"
aria-describedby={id + "_label"}
>
<legend className="govuk-fieldset__legend govuk-fieldset__legend--s govuk-!-font-weight-regular">
<label
className="govuk-fieldset__heading govuk-body-m"
id={id + "_label"}
>
{translatedLabel}
</label>
</legend>
{touched && error && (
<span
id={id + "-error"}
className="govuk-error-message"
>
<span className="govuk-visually-hidden">
Error:
</span>{" "}
{errorMsg}
</span>
)}
<div
className={
custom.inline == false
? "govuk-radios govuk-radios--small"
: "govuk-radios govuk-radios--inline govuk-radios--small"
}
>
{Object.keys(options).map((key, index) => (
<div
className="govuk-radios__item"
data-children-count={key}
key={key}
>
<Field
id={id + "_" + index}
name={id}
component="input"
type="radio"
className="govuk-radios__input"
value={
options[key] == "Yes" ||
options[key] == "Ydw"
? true
: false
}
checked={
typeof value != "undefined" &&
value ==
(options[key] == "Yes" ||
options[key] == "Ydw"
? "true"
: "false")
}
onChange={(e) => {
let docListObj = custom.documentList;
if (
custom.requiredDocumentValue ==
"Yes" ||
custom.requiredDocumentValue ==
"Ydw" ||
custom.requiredDocumentValue ==
"true"
) {
e.target.value == "Ydw" ||
e.target.value == "Yes" ||
e.target.value == "true"
? (docListObj = Object.assign(
docListObj,
{
["documentCheckList_" +
id]:
custom.requiredDocumentLabel
}
))
: delete docListObj[
"documentCheckList_" + id
];
custom.setDocumentsList(docListObj);
}
}}
/>
<label
className="govuk-label govuk-radios__label"
htmlFor={id + "_" + index}
>
{options[key]}
</label>
</div>
))}
</div>
</fieldset>
</div>
</>
);
};
+66
View File
@@ -0,0 +1,66 @@
import React from "react";
import _ from "lodash";
import useTranslation from "next-translate/useTranslation";
import { useRouter } from "next/router";
import { Field } from "redux-form";
import { RenderYesNo } from "./renderYesNo";
export function YesNofield(props) {
const router = useRouter();
let { t } = useTranslation();
const { parentField, form, formProps, parentFieldShowOnValue } = props;
const yesNo = router.locale == "cy" ? ["Ydw", "Na"] : ["Yes", "No"];
const required = (value) => (value ? undefined : "Required");
const showIfHasParentShowValue =
parentField != false &&
!_.isEmpty(formProps[form]) &&
_.has(formProps[form].values, parentField) &&
parentFieldShowOnValue.indexOf(
formProps[form].values[parentField].toString()
) > -1;
return (
<>
{parentField != false ? (
showIfHasParentShowValue && (
<Field
name={props.name}
datafieldname={props.name}
label={props.label}
options={yesNo}
id={props.name}
className={"govuk-radios__input"}
validate={[required]}
errorMsg={t("newappeal:select-an-option-label")}
component={RenderYesNo}
inline={_.has(props, "inline") ? props.inline : "true"}
requiredDocumentLabel={props.requiredDocumentLabel}
requiredDocumentValue={props.requiredDocumentValue}
setDocumentsList={props.setDocumentsList}
documentList={props.documentList}
/>
)
) : (
<Field
name={props.name}
datafieldname={props.name}
label={props.label}
options={yesNo}
id={props.name}
className={"govuk-radios__input"}
validate={[required]}
errorMsg={t("newappeal:select-an-option-label")}
component={RenderYesNo}
inline={_.has(props, "inline") ? props.inline : "true"}
requiredDocumentLabel={props.requiredDocumentLabel}
requiredDocumentValue={props.requiredDocumentValue}
setDocumentsList={props.setDocumentsList}
documentList={props.documentList}
/>
)}
</>
);
}
@@ -0,0 +1,110 @@
export const getThumbnailIconByMimeType = (fileType) => {
switch (fileType) {
case "text/html":
return "/assets/images/documenttypes/html.png";
case "text/plain":
return "/assets/images/documenttypes/txt.png";
case "application/msword":
return "/assets/images/documenttypes/doc.png";
case "application/pdf":
return "/assets/images/documenttypes/pdf.png";
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
return "/assets/images/documenttypes/docx.png";
case "text/csv":
return "/assets/images/documenttypes/csv.png";
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
return "/assets/images/documenttypes/xlsx.png";
case "image/jpeg":
return "/assets/images/documenttypes/jpg.png";
case "image/png":
return "/assets/images/documenttypes/png.png";
default:
return "/assets/images/documenttypes/default.png";
}
};
export const getDocumentTypePrefix = (docCode) => {
const repDate = new Date();
let day = repDate.getDate();
let month = repDate.getMonth() + 1;
let year = repDate.getFullYear();
const pdfFileNameDateStamp =
year + "-" + ("0" + month).slice(-2) + "-" + ("0" + day).slice(-2);
switch (docCode) {
case "000000":
return pdfFileNameDateStamp + "_-_Statement_of_Case";
case "000001":
return pdfFileNameDateStamp + "_-_Application_Form";
case "000002":
return pdfFileNameDateStamp + "_-_Site_Ownership_Certificate";
case "000003":
return pdfFileNameDateStamp + "_-_Decision_Notice";
case "000004":
return pdfFileNameDateStamp + "_-_Site_Location_Plan";
case "000005":
return pdfFileNameDateStamp + "_-_Plans_Drawing_Documents";
case "000006":
return (
pdfFileNameDateStamp + "_-_Additional_Plans_Drawings_Documents"
);
case "000007":
return pdfFileNameDateStamp + "_-_Design_and_Access_Statement";
case "000008":
return pdfFileNameDateStamp + "_-_NSB_LPA_Additional_Documents";
case "000009":
return pdfFileNameDateStamp + "_-_LPA_Correspondence";
case "000010":
return pdfFileNameDateStamp + "_-_LPA_Original_Permission";
case "000011":
return pdfFileNameDateStamp + "_-_LPA's_Registration_Letter";
case "000012":
return pdfFileNameDateStamp + "_-_Environmental_Statement";
case "000013":
return pdfFileNameDateStamp + "_-_Cost_of_Application";
case "000014":
return pdfFileNameDateStamp + "_-_Other_Relevant_Material";
case "000015":
return (
pdfFileNameDateStamp +
"_-_S106_Agreement_or_Unilateral_Undertaking"
);
default:
return pdfFileNameDateStamp + "_-_000000_-_";
}
};
const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-:()—']+$/;
export const validateUploadFilename = (
file,
t,
invalidFilenameLabelKey = "newappeal:new-appeal-fileupload-file-error-invalid-filename-label"
) => {
const name = file.name;
if (name.includes("#")) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(invalidFilenameLabelKey)}`
};
}
if (!FILENAME_ALLOWED.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(invalidFilenameLabelKey)}`
};
}
if (/[<>"/\\|?*]/.test(name)) {
return {
code: "filename-invalid-chars",
message: `${name} ${t(invalidFilenameLabelKey)}`
};
}
return null;
};
@@ -0,0 +1,36 @@
import { JSONPath as jsonpath } from "jsonpath-plus";
export const getFieldTranslation = ({
label,
locale,
appealtypes,
fieldLookup
}) => {
let formObj = jsonpath({
path: "$['" + appealtypes + "']",
json: fieldLookup,
eval: true
});
return locale == "cy"
? jsonpath({
path: '$..[?(@ && @.value=="' + label + '")].value_cy',
json: formObj,
eval: true
})[0]
: label;
};
export const getPickListTranslation = ({
optionValue,
locale,
pickListLookup
}) => {
return locale == "cy"
? jsonpath({
path: '$..[?(@ && @.value=="' + optionValue + '")].value_cy',
json: pickListLookup,
eval: true
})
: optionValue;
};
File diff suppressed because it is too large Load Diff
+5 -6
View File
@@ -64,7 +64,7 @@ const Header = (props) => {
"/myportal/contactus",
"/unsubscribe/[watchlistid]",
"/unsubscribeall/[watchlistid]",
"/status",
"/status"
];
const hasContactLink = [
@@ -72,7 +72,7 @@ const Header = (props) => {
"/dns/help",
"/dns/contact-us",
"/dns/applications",
"/dns/application-view",
"/dns/application-view"
];
let domainSwitch;
@@ -91,7 +91,7 @@ const Header = (props) => {
destroyCookie(null, "pedw_locale", { path: "/" }),
destroyCookie(null, "pinsUser", { path: "/" }),
signOut({
callbackUrl: locale == "cy" ? "/cy/allgofnodi" : "/logout",
callbackUrl: locale == "cy" ? "/cy/allgofnodi" : "/logout"
}));
};
@@ -136,8 +136,7 @@ const Header = (props) => {
// Set cookie first
setCookie(null, "pedw_locale", newLocale, {
path: "/",
maxAge: 60 * 60 * 24 * 365, // 1 year
path: "/"
});
// Then navigate with the new locale
@@ -270,7 +269,7 @@ const mapDispatchToProps = (dispatch) => {
return {
setLogout: () => {
dispatch(setLogout());
},
}
};
};
+385
View File
@@ -18,6 +18,88 @@ Follow-ups:
---
### CL-00X: 22500 `components/elements/index.js` Phase 1 helper extraction
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`, `components/elements/helpers/fileUploadHelpers.js`, `components/elements/helpers/translationHelpers.js`, `memory-bank/refactor-backlog.md`
type: change
rationale: Execute Phase 1 of the approved `components/elements/index.js` decomposition plan by extracting pure helper logic only, reducing monolith coupling while preserving UI/component behavior.
impact: No route/API contract changes; refactor-only extraction of translation/file-upload helper functions with expected behavior parity for EN/CY field labels, file naming, thumbnail icon mapping, and filename validation.
status: completed
Summary:
- Created work branch from `origin/SIPS-Development`: `22500-elements-index-phase1`.
- Added helper modules:
- `components/elements/helpers/fileUploadHelpers.js`
- `getThumbnailIconByMimeType`
- `getDocumentTypePrefix`
- `validateUploadFilename`
- `components/elements/helpers/translationHelpers.js`
- `getFieldTranslation`
- `getPickListTranslation`
- Updated `components/elements/index.js` to consume these helpers and removed duplicated inline helper implementations.
- Kept field renderer/component placement and external prop contracts unchanged (Phase 1 non-goals respected).
- Updated `memory-bank/refactor-backlog.md` with a phased Priority 6 track and Phase 1 guardrail-aligned acceptance criteria.
Validation:
- `npx eslint components/elements/index.js components/elements/helpers/fileUploadHelpers.js components/elements/helpers/translationHelpers.js` -> pass
Follow-ups:
- Phase 2: extract low-risk leaf field renderer components from `components/elements/index.js` in bounded slices.
- Perform manual EN/CY + a11y smoke matrix on new appeal/myportal form journeys before merge.
### CL-00Y: 22500 `components/elements/index.js` Phase 2 leaf renderer extraction (Rich multiline)
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`, `components/elements/fields/renderRichMultiline.js`
type: change
rationale: Continue the approved phased decomposition by extracting one low-risk leaf renderer (`RenderRichMultiline`) from the elements monolith while keeping existing field wiring and behavior intact.
impact: Refactor-only move of rich multiline renderer implementation; no route/API/auth/security changes and no intended EN/CY behavior change.
status: completed
Summary:
- Added `components/elements/fields/renderRichMultiline.js` containing the extracted `RenderRichMultiline` renderer.
- Updated `components/elements/index.js` to import the extracted renderer and removed the inline duplicate implementation.
- Kept `RichMultiLinefield` usage and props unchanged (same Redux Field component wiring and validation flow).
Validation:
- `npx eslint components/elements/index.js components/elements/fields/renderRichMultiline.js` -> pass
Follow-ups:
- Continue Phase 2 in bounded slices by extracting additional low-risk leaf renderers (e.g., `RenderMultiline` / `RenderTextfield`) with no behavior change.
### CL-00Z: 22500 `components/elements/index.js` Phase 2 leaf renderer extraction (Text + Multiline)
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`, `components/elements/fields/renderTextfield.js`, `components/elements/fields/renderMultiline.js`
type: change
rationale: Complete the requested next bounded phase by extracting the additional low-risk leaf renderers (`RenderTextfield`, `RenderMultiline`) from the elements monolith into dedicated field modules while preserving existing wiring and behavior.
impact: Refactor-only move of two renderer components; no intended changes to auth/API/security and no intended EN/CY behavior change.
status: completed
Summary:
- Added `components/elements/fields/renderTextfield.js` and `components/elements/fields/renderMultiline.js`.
- Updated `components/elements/index.js` to import the extracted renderers.
- Removed inline `RenderTextfield` and `RenderMultiline` implementations from `index.js`.
Validation:
- `npx eslint components/elements/index.js components/elements/fields/renderTextfield.js components/elements/fields/renderMultiline.js` -> pass
Follow-ups:
- Continue Phase 2 by selecting the next lowest-risk leaf renderer extraction in a separate commit.
### CL-001: TASK22211 endpoint search-document contract consistency slice
date: 2026-03-23
@@ -3313,3 +3395,306 @@ Validation:
Follow-ups:
- Optional: localize a dedicated `upload-progress-x-of-y` translation key if copy needs stronger grammatical control per locale.
---
### CL-094: 22500 `components/elements/index.js` Phase 2 bounded cleanup (remove dead `renderField`)
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`
type: change
rationale: Execute the next smallest low-risk Phase 2 slice by removing the local `renderField` utility after confirming it is unused in the repo.
impact: No behavior change intended; dead code removal only. No auth/security/middleware/API changes. EN/CY and accessibility behavior remain unchanged.
status: completed
Summary:
- Confirmed `renderField` had no usages outside its declaration.
- Removed the unused local `renderField` function from `components/elements/index.js`.
- Kept all field component exports, routing, and existing render paths unchanged.
Validation:
- `npx eslint components/elements/index.js` -> pass
Follow-ups:
- Continue Phase 2 with one bounded no-behavior-change slice, likely next lowest-risk renderer extraction from `components/elements/index.js`.
---
### CL-095: 22500 `components/elements/index.js` hook-order hotfix (`MultiLinefield`)
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`
type: change
rationale: Fix runtime React warning caused by conditional hook execution path in `MultiLinefield` after dead-code cleanup.
impact: No functional behavior change intended; resolves Rules of Hooks ordering warning by making label translation hook usage unconditional per render.
status: completed
Summary:
- Root cause: `FieldsTranslations(props.label)` (which internally uses `useRouter`) was invoked within conditional render branches in `MultiLinefield`, causing hook order mismatch when branch conditions changed.
- Fix: precomputed `translatedLabel` once in `MultiLinefield` render body and reused in both branches.
- Kept EN/CY text resolution logic and rendered output unchanged.
Validation:
- `npx eslint components/elements/index.js` -> pass
Follow-ups:
- Continue bounded Phase 2 slices; when touching field components, prefer top-level computed hook-backed values reused across conditional branches.
---
### CL-096: 22500 `RenderSubFields` render-phase update warning hotfix
date: 2026-04-07
author: Cline
scope: `components/elements/fields/renderSubFields.js`
type: change
rationale: Fix React warning about updating parent-connected state during `RenderSubFields` render.
impact: No intended behavior change; initial empty FieldArray row initialization moved out of render phase to effect phase to satisfy React rendering constraints.
status: completed
Summary:
- Root cause: `fields.length == 0 && fields.push({})` executed inside render, triggering state updates while rendering `RenderSubFields`.
- Fix: moved initial row insertion into `useEffect`, guarded by `fields.length === 0`.
- Preserved existing UX intent: ensure at least one subfield row appears when array starts empty.
Validation:
- `npx eslint components/elements/fields/renderSubFields.js` -> pass
Follow-ups:
- Keep redux-form `fields.push/remove` calls event/effect-driven (not render-driven) in future slices.
---
### CL-097: 22500 `FieldArrayForm` render-phase dispatch warning hotfix
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`
type: change
rationale: Fix React warning caused by dispatching redux-form state updates during `FieldArrayForm` render.
impact: No intended behavior change; clearing hidden FieldArray values remains intact but now executes in effect phase instead of render phase.
status: completed
Summary:
- Root cause: `dispatch(change("appealForm", name, null))` was called inline in render when parent condition was false.
- Fix: moved that dispatch into `useEffect` guarded by `!showIfHasParentShowValue`.
- Added `useEffect` import in `components/elements/index.js`.
Validation:
- `npx eslint components/elements/index.js` -> pass
Follow-ups:
- Continue avoiding dispatch/state mutations inside render for field visibility toggles.
---
### CL-098: 22500 `FieldArrayForm` bounded dead-code cleanup
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`
type: change
rationale: Continue bounded Phase 2 cleanup with a lowest-risk slice by removing unused locals/destructured props in `FieldArrayForm`.
impact: No intended behavior change; purely removes unused values left from legacy implementation.
status: completed
Summary:
- Removed unused destructured props from `FieldArrayForm`: `label`, `validation`, `maxFieldLength`.
- Removed no-op/dead lines in `FieldArrayForm`:
- redundant boolean expression line
- unused `parentFieldShowOnValue;` expression
- unused `handleSubmit/pristine/reset/submitting` destructure
- Kept visibility logic, effect-driven clearing behavior, and `FieldArray` rendering path unchanged.
Validation:
- `npx eslint components/elements/index.js` -> pass
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).
---
### CL-100: 22500 `components/elements/index.js` helper normalization (validation messages + visibility checks)
date: 2026-04-07
author: Cline
scope: `components/elements/index.js`
type: change
rationale: Apply the requested next bounded refactor slice by consolidating repeated validation message setup and parent-field visibility logic into shared local helpers.
impact: No intended behavior change; EN/CY and accessibility behavior preserved while reducing repeated logic and future drift risk.
status: completed
Summary:
- Added `getValidationMessages(t)` helper for repeated `required/emoji/postcode` message retrieval.
- Added visibility helpers:
- `hasParentFieldValue(...)`
- `isVisibleByEquality(...)`
- `isVisibleByInclusion(...)`
- Replaced repeated inline visibility and validation-message setup across field wrappers with helper usage (Textfield, MultiLinefield, RichMultiLinefield, DateFieldPicker, YesNofield, Radiofield, NumericField, DecimalField).
- Kept existing field render paths, conditions, and validation calls intact.
Validation:
- `npx eslint components/elements/index.js` -> pass
Follow-ups:
- Continue bounded no-behavior-change slices by removing dead locals/comments and extracting one additional low-risk field wrapper at a time.
---
### CL-101: 22500 `PickList` wrapper extraction from `components/elements/index.js`
date: 2026-04-08
author: Cline
scope: `components/elements/index.js`, `components/elements/fields/pickListField.js`
type: change
rationale: Execute one bounded Phase 2 renderer/wrapper extraction slice by moving the `PickList` wrapper out of the elements monolith into a dedicated field module.
impact: No intended behavior change; preserves existing EN/CY translation behavior and validation wiring while reducing `index.js` size/coupling.
status: completed
Summary:
- Added `components/elements/fields/pickListField.js` and moved the existing `PickList` wrapper implementation.
- Updated `components/elements/index.js` to import/export `PickList` from the new field module.
- Removed inline `PickList` wrapper implementation from `index.js`.
- Removed now-unused `RenderPickList` import from `index.js` after extraction.
Validation:
- `npx eslint components/elements/index.js components/elements/fields/pickListField.js` -> pass
Follow-ups:
- Continue bounded no-behavior-change slices by extracting one additional low-risk wrapper (e.g., `CheckBoxfield`) or removing dead locals/debug logging in place.
---
### CL-102: 22500 `components/elements/index.js` dead/debug-only cleanup slice
date: 2026-04-08
author: Cline
scope: `components/elements/index.js`
type: change
rationale: Execute requested bounded cleanup slice by removing dead code and debug-only artifacts from the elements monolith without changing behavior.
impact: No intended behavior change; EN/CY and accessibility behavior preserved while reducing noise and unused code paths.
status: completed
Summary:
- Removed debug-only runtime log in `CheckBoxfield` (`console.log(props)`).
- Removed unused/dead locals and helpers inside `components/elements/index.js`, including:
- top-level unused imports (`axios`, `pickListLookup`, `setFileCount` action import, redux hooks import)
- unused local validators and helpers in wrappers (e.g., unused `required`/`postcode`/`normalizeDecimal` variants)
- unused placeholder constant `RenderCaseID`
- unused local fallbacks in `FileUploadField` (`uploadCount`, local `setFileCount`)
- Kept functional field wiring, labels/translations, and validation behavior in active render paths unchanged.
Validation:
- `npx eslint components/elements/index.js` -> pass
Follow-ups:
- Continue bounded no-behavior-change slices only (e.g., extract one additional low-risk wrapper such as `CheckBoxfield`).
---
### CL-103: 22500 wrapper extraction bundle (`CheckBoxfield`, `DateFieldPicker`, `YesNofield`)
date: 2026-04-08
author: Cline
scope: `components/elements/index.js`, `components/elements/fields/{checkBoxField,dateFieldPicker,yesNoField}.js`
type: change
rationale: Execute the requested bundled wrapper slice by extracting wrappers 1/2/3 in one commit while keeping behavior unchanged.
impact: No intended behavior change; keeps EN/CY output, visibility logic, and accessibility structure intact while reducing `components/elements/index.js` size.
status: completed
Summary:
- Added `components/elements/fields/checkBoxField.js` and moved `CheckBoxfield` wrapper.
- Added `components/elements/fields/dateFieldPicker.js` and moved `DateFieldPicker` wrapper logic.
- Added `components/elements/fields/yesNoField.js` and moved `YesNofield` wrapper logic.
- Updated `components/elements/index.js` to import/export these wrappers from field modules.
- Removed inline implementations of `CheckBoxfield`, `DateFieldPicker`, and `YesNofield` from `index.js`.
Validation:
- `npx eslint components/elements/index.js components/elements/fields/checkBoxField.js components/elements/fields/dateFieldPicker.js components/elements/fields/yesNoField.js` -> pass
Follow-ups:
- Remaining wrappers can continue as bounded slices (`Radiofield`, `NumericField`, `DecimalField`) if required.
---
### CL-104: 22500 wrapper extraction bundle (`Radiofield`, `NumericField`, `DecimalField`)
date: 2026-04-08
author: Cline
scope: `components/elements/index.js`, `components/elements/fields/{radioField,numericField,decimalField}.js`
type: change
rationale: Continue Phase 2 with the next bounded wrapper bundle by extracting wrappers 4/5/6 from `components/elements/index.js` into dedicated field modules without behavior change.
impact: No intended behavior change; preserves EN/CY behavior, validation wiring, and accessibility semantics while reducing monolith size.
status: completed
Summary:
- Added `components/elements/fields/radioField.js` for `Radiofield`.
- Added `components/elements/fields/numericField.js` for `NumericField`.
- Added `components/elements/fields/decimalField.js` for `DecimalField`.
- Updated `components/elements/index.js` to import/export these wrappers from field modules.
- Removed inline `Radiofield`, `NumericField`, and `DecimalField` implementations from `index.js`.
Validation:
- `npx eslint components/elements/index.js components/elements/fields/radioField.js components/elements/fields/numericField.js components/elements/fields/decimalField.js` -> pass
Follow-ups:
- Next bounded wrappers (if needed): `ReadOnlyfield`/remaining small wrappers or additional dead-code cleanup slices.
+53
View File
@@ -65,6 +65,59 @@ Last updated: 2026-03-12
3. hash utility behavior
4. locale rewrite mapping sanity checks
## Priority 6 — Decompose `components/elements/index.js` monolith (phased)
- **Problem:** `components/elements/index.js` has grown into a high-coupling UI monolith (~2700+ LOC) combining field primitives, validation/conditional logic, translation helpers, file-upload orchestration, and field-array behavior.
- **Why it matters:** Very high regression surface for new appeal/my portal forms, slower change velocity, and poor testability/isolation.
- **Target outcome:** `components/elements/index.js` reduced to a thin barrel export with responsibility split into focused modules.
- **Context alignment:** Matches architecture direction in `context/architecture.md` (endpoint/component sprawl reduction, bounded slices) and file boundary guidance in `context/coding-conventions.md` (`pages` thin, reusable logic/components split by concern).
- **Guardrail constraints:**
- Preserve public-service reliability for core flows (`search`, `case`, `myportal`, `newappeal`).
- No auth/session/security header behavior changes (out of scope).
- Maintain EN/CY parity for user-facing behavior.
- Keep accessibility behavior unchanged (labels, focus, keyboard flow, errors).
### Priority 6 — Phase 1 (start here): extract pure helpers only
- **Scope (Phase 1 only):**
- Move pure/helper logic from `components/elements/index.js` into focused helper modules under `components/elements/` (or `components/elements/helpers/`) without behavior change.
- Candidate helper extraction set:
- translation helpers (`FieldsTranslations`, picklist translation helper)
- file upload helper utilities (icon/doc type naming/pure format helpers)
- other deterministic pure functions used by field renderers
- Keep all field renderers/components in place for Phase 1.
- **Non-goals (Phase 1):**
- No JSX component relocation yet.
- No upload flow logic rewrites.
- No validation rule behavior changes.
- No prop contract changes for existing consumers.
- **Acceptance criteria (Phase 1):**
- `components/elements/index.js` imports extracted helpers from new helper modules and behavior remains equivalent.
- No route/API changes.
- Existing new appeal + myportal form journeys continue to function in EN and CY.
- Accessibility smoke unchanged for touched form controls (label association, keyboard reachability, inline error visibility).
- Lint passes for touched files.
- **Validation matrix (minimum):**
1. `npm run lint`
2. Manual smoke:
- new appeal form step rendering + validation messages
- myportal representation/new appeal editing flow controls
- file upload field icon/name behavior unchanged
3. Locale parity checks (EN/CY) for touched user-facing labels/routes.
4. A11y smoke checks on touched fields (focus, labels, errors).
- **Rollback plan (Phase 1):**
- Revert helper module extraction commit(s) to restore single-file implementation.
- No migration/data rollback required.
- **Next phases (for tracking):**
- **Phase 2:** extract low-risk leaf field renderer components.
- **Phase 3:** extract `RenderFileUpload` and upload container.
- **Phase 4:** extract field-array/repeater components.
- **Phase 5:** finalize `components/elements/index.js` as barrel-only export.
## Sequencing recommendation
1. Priorities 2 + 4 (security/integrity foundation)
+79 -44
View File
@@ -15,9 +15,12 @@ import { PrismaClient } from "@prisma/client";
import NextAuth from "next-auth";
import EmailProvider from "next-auth/providers/email";
import { consoleLogger } from "../../../actions/core/logger";
import { getPortalLogin } from "../../../actions/services/accountService";
const prisma = new PrismaClient();
const WELSH_LANGUAGE_CODE = 846040000;
const appendParamsAndPathToNewUrl = (fromUrl, toUrl) => {
const fromUrlObj = new URL(fromUrl);
const params = fromUrlObj.searchParams;
@@ -34,14 +37,64 @@ const appendParamsAndPathToNewUrl = (fromUrl, toUrl) => {
return toUrlObj.toString();
};
const resolveLocale = (req) =>
const resolveRequestLocale = (req) =>
req?.query?.locale ||
req?.body?.locale ||
req?.cookies?.pedw_locale ||
"en";
const resolveCrmLocale = async (email) => {
if (!email) return null;
try {
const portalUserObj = await getPortalLogin(email);
const preferredLanguage =
portalUserObj?.value?.[0]?.pinswg_preferredlanguage;
if (preferredLanguage === WELSH_LANGUAGE_CODE) {
return "cy";
}
if (preferredLanguage != null) {
return "en";
}
return null;
} catch (error) {
consoleLogger(error);
return null;
}
};
const resolveEffectiveLocale = async (req, email) => {
const crmLocale = await resolveCrmLocale(email);
if (crmLocale) return crmLocale;
return resolveRequestLocale(req);
};
const buildLocalizedVerificationUrl = ({ url, email, effectiveLocale }) => {
const { host, protocol, searchParams } = new URL(url);
const baseDomain = `${protocol}//${host}`;
const newURL =
effectiveLocale === "cy"
? baseDomain +
"/api/auth/callback/email?callbackUrl=" +
encodeURIComponent(baseDomain + "/cy") +
"&token=" +
searchParams.get("token") +
"&email=" +
encodeURIComponent(email) +
"&locale=" +
effectiveLocale
: url;
return appendParamsAndPathToNewUrl(url, newURL);
};
const authOptions = (req, res) => {
const locale = resolveLocale(req);
const requestLocale = resolveRequestLocale(req);
return {
providers: [
@@ -50,7 +103,10 @@ const authOptions = (req, res) => {
name: "emailAPI",
type: "email",
async sendVerificationRequest({ identifier: email, url }) {
const { host, protocol, searchParams } = new URL(url);
const effectiveLocale = await resolveEffectiveLocale(
req,
email
);
console.log(
"============================================================\n",
@@ -58,23 +114,11 @@ const authOptions = (req, res) => {
"============================================================\n"
);
const baseDomain = `${protocol}//${host}`;
const effectiveLocale = resolveLocale(req);
const newURL =
effectiveLocale === "cy"
? baseDomain +
"/api/auth/callback/email?callbackUrl=" +
encodeURIComponent(baseDomain + "/cy") +
"&token=" +
searchParams.get("token") +
"&email=" +
encodeURIComponent(email) +
"&locale=" +
effectiveLocale
: url;
const formURL = appendParamsAndPathToNewUrl(url, newURL);
const formURL = buildLocalizedVerificationUrl({
url,
email,
effectiveLocale
});
console.log(
"============================================================\n",
@@ -90,12 +134,13 @@ const authOptions = (req, res) => {
EmailProvider({
maxAge: 2 * 60 * 60,
async sendVerificationRequest({ identifier: email, url }) {
const { host, protocol, searchParams } = new URL(url);
const baseDomain = `${protocol}//${host}`;
const templateId = "b1b5704b-9bb8-4deb-a75c-d887ca902661";
const templateIdcy = "0614ce53-cd5f-421f-a1a5-8c8486a9113a";
const effectiveLocale = resolveLocale(req);
const effectiveLocale = await resolveEffectiveLocale(
req,
email
);
console.log(
"============================================================\n",
@@ -103,20 +148,11 @@ const authOptions = (req, res) => {
"============================================================\n"
);
const newURL =
effectiveLocale === "cy"
? baseDomain +
"/api/auth/callback/email?callbackUrl=" +
encodeURIComponent(baseDomain + "/cy") +
"&token=" +
searchParams.get("token") +
"&email=" +
encodeURIComponent(email) +
"&locale=" +
effectiveLocale
: url;
const formURL = appendParamsAndPathToNewUrl(url, newURL);
const formURL = buildLocalizedVerificationUrl({
url,
email,
effectiveLocale
});
console.log(
"============================================================\n",
@@ -178,11 +214,11 @@ const authOptions = (req, res) => {
}
},
pages: {
signIn: (locale === "cy" ? "/cy" : "") + "/auth/signin",
error: (locale === "cy" ? "/cy" : "") + "/auth/error",
signIn: (requestLocale === "cy" ? "/cy" : "") + "/auth/signin",
error: (requestLocale === "cy" ? "/cy" : "") + "/auth/error",
verifyRequest:
(locale === "cy" ? "/cy" : "") + "/auth/verify-request",
newUser: (locale === "cy" ? "/cy" : "") + "/account/register"
(requestLocale === "cy" ? "/cy" : "") + "/auth/verify-request",
newUser: (requestLocale === "cy" ? "/cy" : "") + "/account/register"
},
callbacks: {
session: async (session, user) => {
@@ -196,9 +232,8 @@ const authOptions = (req, res) => {
if (url.startsWith("/")) return `${baseUrl}${url}`;
if (new URL(url).origin === baseUrl) return url;
const effectiveLocale = resolveLocale(req);
const newUrl =
effectiveLocale === "cy"
requestLocale === "cy"
? process.env.CY_API_ROOT
: process.env.NEXTAUTH_URL;
+43
View File
@@ -0,0 +1,43 @@
import { getPortalLogin } from "../../../actions/services/accountService";
import { consoleLogger } from "../../../actions/core/logger";
const WELSH_LANGUAGE_CODE = 846040000;
const resolveRequestLocale = (req) =>
req?.query?.locale ||
req?.body?.locale ||
req?.cookies?.pedw_locale ||
"en";
export default async function handler(req, res) {
if (req.method !== "POST") {
return res.status(405).json({ message: "Method not allowed" });
}
const email = String(req.body?.email || "")
.trim()
.toLowerCase();
const sessionLocale = resolveRequestLocale(req);
if (!email) {
return res.status(200).json({ locale: sessionLocale });
}
try {
const portalUserObj = await getPortalLogin(email);
const preferredLanguage =
portalUserObj?.value?.[0]?.pinswg_preferredlanguage;
const locale =
preferredLanguage === WELSH_LANGUAGE_CODE
? "cy"
: preferredLanguage != null
? "en"
: sessionLocale;
return res.status(200).json({ locale });
} catch (error) {
consoleLogger(error);
return res.status(200).json({ locale: sessionLocale });
}
}
+1 -1
View File
@@ -64,7 +64,7 @@ export default async function ApiProxy(req, res) {
const queryUrl =
"contacts?$filter=emailaddress1 eq '" +
emailAddress +
"' and statuscode eq 1&$count=true&$select=emailaddress1,contactid,yomifullname,firstname,lastname";
"' and statuscode eq 1&$count=true&$select=pinswg_preferredlanguage,emailaddress1,contactid,yomifullname,firstname,lastname";
const relayPolicy = RELAY_POLICY_STRICT_LOGIN;
+4 -1
View File
@@ -10,7 +10,10 @@ import fs from "fs";
import path from "path";
const FILENAME_ALLOWED = /^[A-Za-z0-9 ._\-:()—']+$/;
const MAX_FILES_PER_UPLOAD_BATCH = process.env.UPLOAD_BATCH_COUNT || 5;
const MAX_FILES_PER_UPLOAD_BATCH = Math.max(
1,
Number.parseInt(process.env.UPLOAD_BATCH_COUNT || "5", 10) || 5
);
function validateFilenameServer(originalFilename) {
const name = path.basename(originalFilename || "");
+40 -9
View File
@@ -19,17 +19,48 @@ const SignIn = (props) => {
event.preventDefault();
setButtonDisabled(true);
const currentLocale = lang || "en";
const url = new URL(event.target.callbackUrl.value);
url.searchParams.set("locale", currentLocale);
try {
const email = String(event.target.email.value || "").trim();
const currentLocale = lang || "en";
setCookie(null, "pedw_locale", currentLocale, {
path: "/",
maxAge: 60 * 60 * 24 * 365
});
const response = await fetch("/api/auth/resolve-locale", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
email,
locale: currentLocale
})
});
event.target.callbackUrl.value = url.toString();
event.target.submit();
const data = await response.json();
const resolvedLocale =
data?.locale === "cy" || data?.locale === "en"
? data.locale
: currentLocale;
const url = new URL(event.target.callbackUrl.value);
url.searchParams.set("locale", resolvedLocale);
setCookie(null, "pedw_locale", resolvedLocale, {
path: "/"
});
event.target.callbackUrl.value = url.toString();
event.target.submit();
} catch (error) {
const fallbackLocale = lang || "en";
const url = new URL(event.target.callbackUrl.value);
url.searchParams.set("locale", fallbackLocale);
setCookie(null, "pedw_locale", fallbackLocale, {
path: "/"
});
event.target.callbackUrl.value = url.toString();
event.target.submit();
}
};
return (