## Representations Refactor — Behaviour-Preserving Structural Improvements This PR delivers a full refactor of the representations flow, improving structure, readability, and maintainability while preserving all existing behaviour. The work was completed using a controlled, slice-based approach with strict guardrails and regression validation at each step. No changes have been made to user journeys, payloads, routing, or EN/CY behaviour. The result is a cleaner, more maintainable codebase with reduced coupling and clearer separation of concerns, ready for future enhancements without increased risk. --- ## What Was Done The refactor was delivered incrementally across the following slices: - **R1** — Representation entry logic extraction - **R2** — Page loader separation (SSR/data orchestration) - **R3** — Journey step resolution extraction - **R4** — Flow shell decomposition - **R5** — Representation elements normalisation - **R6** — Data/service layer cleanup - **R7** — Summary rendering proof slice - **R8** — Submission/finalisation boundary isolation - **R9** — Summary rollout (Batch 1) Each slice: - was isolated to a single concern - followed strict guardrails - was validated before merge Full detail is available in: `context/representations-refactor-tracker.md` --- ## Key Improvements - Reduced coupling across the representations journey - Separated data loading, orchestration, and rendering concerns - Simplified complex conditional logic into testable helpers - Standardised summary rendering using shared primitives (`SummaryCard`, `SummaryRow`) - Isolated submission/finalisation sequencing into explicit boundaries - Improved overall readability and maintainability --- ## Behaviour Preservation This refactor does **not** change: - User journeys (APP / IP / Agent / LPA) - Route and query behaviour - Payload contracts and API interactions - Redux state shape and usage - Validation rules and messaging - EN/CY behaviour - File upload / PDF / email sequencing - Linked-case logic All changes are structural only. --- ## Validation ### Automated - `npm run lint` — passed (warnings only, no new errors) - `npm run test:reps` — passed (7/7) ### Manual Validated end-to-end across: - APP - IP - Agent - LPA Including: - representation creation - editing/resuming representations - submission flow - confirmation/completion behaviour - summary rendering across case types - EN/CY parity --- ## Risk Management The refactor targeted several high-risk areas: - Case summary entry logic - Representation submission/finalisation sequencing - Dual-mode entry (new vs existing representation) Risk was controlled through: - small, incremental slices - one branch per slice - regression validation per slice - strict behaviour-preservation guardrails - controlled rollout for summary rendering changes --- ## Reviewer Guidance Suggested areas to focus on: - End-to-end representation journey (create → submit → complete) - S...
264 lines
12 KiB
JavaScript
264 lines
12 KiB
JavaScript
import useTranslation from "next-translate/useTranslation";
|
|
import { useRouter } from "next/router";
|
|
import { connect } from "react-redux";
|
|
import { Field, change, reduxForm } from "redux-form";
|
|
import { useState } from "react";
|
|
|
|
import { bytesToSize, getThumbnailIconByExtension } from "../../utils";
|
|
import {
|
|
FileUploadField,
|
|
RenderPickList,
|
|
RenderRichMultiline,
|
|
shouldDisableButton
|
|
} from "./representationElements";
|
|
import RepresentationActionButtons from "./elements/RepresentationActionButtons";
|
|
import RepresentationTypeSelectorBlock from "./elements/RepresentationTypeSelectorBlock";
|
|
|
|
import { useDropzone } from "react-dropzone";
|
|
import {
|
|
setRepresentationCapacity,
|
|
setRepresentationSubmit,
|
|
setFilesForRepresentations
|
|
} from "../../../store/currentView/action";
|
|
|
|
const RepAppellant = (props) => {
|
|
let { t } = useTranslation();
|
|
|
|
const router = useRouter();
|
|
const { locale } = router;
|
|
const {
|
|
formObj,
|
|
appellantApplicantRepresentationArr,
|
|
setRepresentationCapacity,
|
|
setRepresentationSubmit,
|
|
currentView,
|
|
setRepFileName,
|
|
updateRepresentation,
|
|
setSavingStatus,
|
|
savingStatus,
|
|
setFilesForRepresentations
|
|
} = props;
|
|
const { acceptedFiles, getRootProps, getInputProps } = useDropzone();
|
|
|
|
console.log(JSON.stringify(formObj) == "{}" ? "empty" : "value");
|
|
|
|
if (JSON.stringify(formObj) != "{}") {
|
|
setRepFileName(formObj.representationForm.values);
|
|
|
|
let filterFilesList = props.currentView.caseReference.hasOwnProperty(
|
|
"repDetails"
|
|
)
|
|
? props.currentView.caseReference.repDetails.hasOwnProperty(
|
|
"filesList"
|
|
)
|
|
? props.currentView.caseReference.repDetails.filesList.filter(
|
|
function (el) {
|
|
return el != null;
|
|
}
|
|
)
|
|
: []
|
|
: [];
|
|
|
|
filterFilesList = filterFilesList.filter(
|
|
(value, index, self) =>
|
|
index === self.findIndex((t) => t.name === value.name)
|
|
);
|
|
|
|
const hasTypeSelectionInline =
|
|
typeof formObj.representationForm.values.representationType ==
|
|
"undefined" && props.representationType != "Select...";
|
|
|
|
const selectedRepresentationTypeDisplay =
|
|
router.locale == "cy"
|
|
? formObj.representationForm.values.representationType ==
|
|
"Questionnaire"
|
|
? "Holiadur"
|
|
: formObj.representationForm.values.representationType ==
|
|
"Statement"
|
|
? "Datganiad"
|
|
: formObj.representationForm.values.representationType ==
|
|
"Final comments"
|
|
? "Sylwadau terfynol"
|
|
: formObj.representationForm.values.representationType
|
|
: formObj.representationForm.values.representationType;
|
|
|
|
const handleSaveExit = () => {
|
|
let valuesObj = props.props.props.form[props.form].values || {};
|
|
|
|
delete valuesObj["_pinswg_appellant_value"];
|
|
|
|
updateRepresentation(valuesObj, false, true);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<RepresentationTypeSelectorBlock
|
|
t={t}
|
|
headingText={t(
|
|
"myrepresentations:representation-from-an-appellant-heading"
|
|
)}
|
|
kindLabelText={t("myrepresentations:kind-of-rep-label")}
|
|
isTypeSelected={!hasTypeSelectionInline}
|
|
selectedTypeDisplay={selectedRepresentationTypeDisplay}
|
|
optionsArr={appellantApplicantRepresentationArr}
|
|
/>
|
|
{(typeof props.representationType != "undefined" ||
|
|
typeof formObj.representationForm.values
|
|
.representationType != "undefined") && (
|
|
<div className="govuk-grid-row">
|
|
<div className="govuk-form-group">
|
|
<p className="govuk-body">
|
|
{t("myrepresentations:enter-comment-label")}
|
|
</p>
|
|
<fieldset
|
|
className="govuk-fieldset"
|
|
aria-describedby="commentBox-hint"
|
|
>
|
|
{/* <div
|
|
id="commentBox-hint"
|
|
className="govuk-hint"
|
|
>
|
|
{t(
|
|
"myrepresentations:comments-set-out-label"
|
|
)}
|
|
:
|
|
</div> */}
|
|
<div className="govuk-form-group">
|
|
<Field
|
|
name="representationComments"
|
|
id="representationComments"
|
|
component={RenderRichMultiline}
|
|
type="text"
|
|
rows="5"
|
|
className="govuk-textarea govuk-input--width-30"
|
|
aria-describedby={props.name + "-hint"}
|
|
/>
|
|
{formObj.representationForm.values
|
|
.appealType == "846040004" ? (
|
|
<p className="govuk-hint govuk-!-font-size-14 govuk-!-margin-top-5">
|
|
{t(
|
|
"myrepresentations:publish-policy-label"
|
|
)}{" "}
|
|
<a
|
|
href={t(
|
|
"myrepresentations:publish-policy-link"
|
|
)}
|
|
>
|
|
{t(
|
|
"myrepresentations:publish-policy-link-label"
|
|
)}
|
|
</a>{" "}
|
|
{t(
|
|
"myrepresentations:publish-policy-label-two"
|
|
)}
|
|
</p>
|
|
) : (
|
|
<p className="govuk-hint govuk-!-font-size-14 govuk-!-margin-top-5">
|
|
{t(
|
|
"myrepresentations:publish-policy-label"
|
|
)}{" "}
|
|
<a
|
|
href={t(
|
|
"myrepresentations:publish-policy-link"
|
|
)}
|
|
>
|
|
{t(
|
|
"myrepresentations:publish-policy-link-label"
|
|
)}
|
|
</a>{" "}
|
|
{t(
|
|
"myrepresentations:publish-policy-label-two"
|
|
)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div className="govuk-form-group govuk-!-margin-bottom-9">
|
|
<FileUploadField
|
|
name={"representationDocuments"}
|
|
id={"representationDocuments"}
|
|
label={t(
|
|
"myrepresentations:add-files-label"
|
|
)}
|
|
ticketnumber={
|
|
props.currentView.caseReference
|
|
.ticketnumber
|
|
}
|
|
hint={"sdsd"}
|
|
fileList={
|
|
props.currentView.fileList
|
|
// props.currentView.caseReference.hasOwnProperty(
|
|
// "repDetails"
|
|
// )
|
|
// ? props.currentView.caseReference.repDetails.hasOwnProperty(
|
|
// "filesList"
|
|
// ) &&
|
|
// props.currentView
|
|
// .caseReference.repDetails
|
|
// .filesList.length > 0
|
|
// ? props.currentView
|
|
// .caseReference
|
|
// .repDetails.filesList
|
|
// : []
|
|
// : []
|
|
}
|
|
setFilesForRepresentations={
|
|
setFilesForRepresentations
|
|
}
|
|
containerID={
|
|
props.props.props.accountDetails
|
|
.containerID
|
|
}
|
|
filenamePrefix={props.formObj}
|
|
props={props.props.props}
|
|
setCurrentReference={
|
|
props.setCurrentReference
|
|
}
|
|
/>
|
|
</div>
|
|
</fieldset>
|
|
</div>{" "}
|
|
</div>
|
|
)}
|
|
|
|
{typeof formObj.representationForm.values.representationType !=
|
|
"undefined" && (
|
|
<RepresentationActionButtons
|
|
t={t}
|
|
locale={router.locale}
|
|
isSaving={savingStatus}
|
|
isContinueDisabled={shouldDisableButton({
|
|
props,
|
|
formObj
|
|
})}
|
|
onSaveExit={handleSaveExit}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
};
|
|
|
|
//const mapStateToProps = (state, props) => {};
|
|
|
|
const mapDispatchToProps = (dispatch) => {
|
|
return {
|
|
updateField: (form, field, newValue) =>
|
|
dispatch(change(form, field, newValue)),
|
|
|
|
setFilesForRepresentations: (fileList) => {
|
|
dispatch(setFilesForRepresentations(fileList));
|
|
}
|
|
};
|
|
};
|
|
|
|
export default connect(
|
|
null,
|
|
mapDispatchToProps
|
|
)(
|
|
reduxForm({
|
|
form: "representationForm",
|
|
enableReinitialize: true,
|
|
destroyOnUnmount: false
|
|
})(RepAppellant)
|
|
);
|