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:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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)}
|
||||
>
|
||||
−
|
||||
</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")}
|
||||
>
|
||||
−
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
+88
-2245
File diff suppressed because it is too large
Load Diff
@@ -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());
|
||||
},
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user