Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRMDR-873 - Enable ARF upload via ARF bucket #367

Merged
merged 21 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2b460ca
[PRMDR-873] Add option to test panel to enter ARF workflow, remove LG…
joefong-nhs May 16, 2024
5da5e78
[PRMDR-873] allow searchPatient lambda to return non active patients …
joefong-nhs May 16, 2024
a1698ee
[PRMDR-873] some refactoring around mock pds
joefong-nhs May 16, 2024
a10580e
[PRMDR-873] Add S3 upload ability to ARF journey
joefong-nhs May 16, 2024
50fe05d
[PRMDR-873] Change create doc ref to use ARF bucket for ARF
joefong-nhs May 17, 2024
d5370d0
[PRMDR-873] improve error handling at arf file select page
joefong-nhs May 17, 2024
2df1d65
[PRMDR-873] ensure env var in create doc ref lambda
joefong-nhs May 17, 2024
7e7ef02
[PRMDR-873] refactor and fix issue around S3 file path
joefong-nhs May 17, 2024
f205863
[PRMDR-873] refactor and fix issue around S3 file path
joefong-nhs May 17, 2024
0f854cb
[PRMDR-873] Add unit test
joefong-nhs May 17, 2024
5a6341b
[PRMDR-873] Keep unused frontend code as comment
joefong-nhs May 17, 2024
df8d2f0
[PRMDR-873] minor fix
joefong-nhs May 17, 2024
455b6f1
[PRMDR-873] Improve test coverage
joefong-nhs May 17, 2024
63aec83
[PRMDR-873] improve coverage
joefong-nhs May 17, 2024
63c8af8
[PRMDR-873] address sonarcloud issue
joefong-nhs May 17, 2024
7e24e9c
[PRMDR-873] improve coverage
joefong-nhs May 17, 2024
d725903
[PRMDR-873] improve coverage
joefong-nhs May 17, 2024
074df76
Merge branch 'main' into PRMDR-873
joefong-nhs May 20, 2024
63c4dff
[PRMDR-873] Remove LG related codes from ARF upload screen
joefong-nhs May 20, 2024
6984cf5
[PRMDR-873] Address PR comment (s3 file location builder)
joefong-nhs May 20, 2024
57a67ef
remove unused import
joefong-nhs May 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
377 changes: 118 additions & 259 deletions app/src/components/blocks/_arf/selectStage/SelectStage.test.tsx

Large diffs are not rendered by default.

215 changes: 128 additions & 87 deletions app/src/components/blocks/_arf/selectStage/SelectStage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { Dispatch, SetStateAction, useRef, useState } from 'react';
import React, { Dispatch, SetStateAction, useRef } from 'react';
import {
DOCUMENT_TYPE,
DOCUMENT_UPLOAD_STATE,
Expand All @@ -12,12 +12,19 @@ import { useController, useForm } from 'react-hook-form';
import toFileList from '../../../../helpers/utils/toFileList';
import PatientSummary from '../../../generic/patientSummary/PatientSummary';
import DocumentInputForm from '../documentInputForm/DocumentInputForm';
import { ARFFormConfig, lloydGeorgeFormConfig } from '../../../../helpers/utils/formConfig';
import { ARFFormConfig } from '../../../../helpers/utils/formConfig';
import { v4 as uuidv4 } from 'uuid';
import uploadDocuments from '../../../../helpers/requests/uploadDocuments';
import uploadDocuments, { uploadDocumentToS3 } from '../../../../helpers/requests/uploadDocuments';
import usePatient from '../../../../helpers/hooks/usePatient';
import useBaseAPIUrl from '../../../../helpers/hooks/useBaseAPIUrl';
import useBaseAPIHeaders from '../../../../helpers/hooks/useBaseAPIHeaders';
import BackButton from '../../../generic/backButton/BackButton';
import { UploadSession } from '../../../../types/generic/uploadResult';
import { AxiosError } from 'axios';
import { routes } from '../../../../types/generic/routes';
import { errorToParams } from '../../../../helpers/utils/errorToParams';
import { isMock } from '../../../../helpers/utils/isLocal';
import { useNavigate } from 'react-router';

interface Props {
setDocuments: SetUploadDocuments;
Expand All @@ -26,123 +33,157 @@ interface Props {
}

function SelectStage({ setDocuments, setStage, documents }: Props) {
const [arfDocuments, setArfDocuments] = useState<Array<UploadDocument>>([]);
const [lgDocuments, setLgDocuments] = useState<Array<UploadDocument>>([]);
const baseUrl = useBaseAPIUrl();
const baseHeaders = useBaseAPIHeaders();
let arfInputRef = useRef<HTMLInputElement | null>(null);
let lgInputRef = useRef<HTMLInputElement | null>(null);
const navigate = useNavigate();
const arfInputRef = useRef<HTMLInputElement | null>(null);
const patientDetails = usePatient();
const nhsNumber: string = patientDetails?.nhsNumber ?? '';
const mergedDocuments = [...arfDocuments, ...lgDocuments];
const hasFileInput = mergedDocuments.length;
const hasFileInput = documents.length > 0;

const { handleSubmit, control, formState } = useForm();
const { handleSubmit, control, formState, setError } = useForm();

const lgController = useController(lloydGeorgeFormConfig(control));
const arfController = useController(ARFFormConfig(control));

const submitDocuments = async () => {
if (!hasFileInput) {
setError('arf-documents', { type: 'custom', message: 'Select a file to upload' });
return;
}

setStage(UPLOAD_STAGE.Uploading);
try {
await uploadDocuments({
const uploadSession = await uploadDocuments({
nhsNumber,
documents,
baseUrl,
baseHeaders,
});
} catch (e) {}
setStage(UPLOAD_STAGE.Complete);
const uploadingDocuments: UploadDocument[] =
addMetadataAndMarkDocumentAsUploading(uploadSession);
setDocuments(uploadingDocuments);

await uploadAllDocumentsToS3(uploadingDocuments, uploadSession);

setStage(UPLOAD_STAGE.Complete);
} catch (error) {
handleUploadError(error as AxiosError);
}
};

const markDocumentAsFailed = (failedDocument: UploadDocument) => {
setDocuments((prevState) =>
prevState.map((prevStateDocument) => {
if (prevStateDocument.id !== failedDocument.id) {
return prevStateDocument;
}
return { ...prevStateDocument, state: DOCUMENT_UPLOAD_STATE.FAILED, progress: 0 };
}),
);
};

const uploadAllDocumentsToS3 = (
uploadingDocuments: UploadDocument[],
uploadSession: UploadSession,
) => {
const allUploadPromises = uploadingDocuments.map((document) =>
uploadDocumentToS3({ setDocuments, document, uploadSession }).catch(() =>
markDocumentAsFailed(document),
),
);
return Promise.all(allUploadPromises);
};

const handleUploadError = (error: AxiosError) => {
if (error.response?.status === 403) {
navigate(routes.SESSION_EXPIRED);
} else if (isMock(error)) {
/* istanbul ignore next */
setDocuments((prevState) =>
prevState.map((doc) => ({
...doc,
state: DOCUMENT_UPLOAD_STATE.SUCCEEDED,
})),
);
/* istanbul ignore next */
setStage(UPLOAD_STAGE.Complete);
} else {
navigate(routes.SERVER_ERROR + errorToParams(error));
}
};

const addMetadataAndMarkDocumentAsUploading = (uploadSession: UploadSession) => {
return documents.map((doc) => {
const documentMetadata = uploadSession[doc.file.name];
const documentReference = documentMetadata.fields.key;
return {
...doc,
state: DOCUMENT_UPLOAD_STATE.UPLOADING,
key: documentReference,
ref: documentReference.split('/').at(-1),
};
});
};

const onInput = (e: FileInputEvent, docType: DOCUMENT_TYPE) => {
const fileArray = Array.from(e.target.files ?? new FileList());
const documentMap: Array<UploadDocument> = fileArray.map((file) => ({
const newlyAddedDocuments: Array<UploadDocument> = fileArray.map((file) => ({
id: uuidv4(),
file,
state: DOCUMENT_UPLOAD_STATE.SELECTED,
progress: 0,
docType: docType,
attempts: 0,
}));
const isArfDoc = docType === DOCUMENT_TYPE.ARF;
const mergeList = isArfDoc ? lgDocuments : arfDocuments;
const docTypeList = isArfDoc ? arfDocuments : lgDocuments;
const updatedDocList = [...documentMap, ...docTypeList];
if (isArfDoc) {
setArfDocuments(updatedDocList);
arfController.field.onChange(updatedDocList);
} else {
setLgDocuments(updatedDocList);
lgController.field.onChange(updatedDocList);
}
const updatedFileList = [...mergeList, ...updatedDocList];
setDocuments(updatedFileList);
const updatedDocList = [...newlyAddedDocuments, ...documents];
arfController.field.onChange(updatedDocList);
setDocuments(updatedDocList);
};

const onRemove = (index: number, docType: DOCUMENT_TYPE) => {
const isArfDoc = docType === DOCUMENT_TYPE.ARF;
const mergeList = isArfDoc ? lgDocuments : arfDocuments;
const docTypeList = isArfDoc ? arfDocuments : lgDocuments;
const updatedDocList = [...docTypeList.slice(0, index), ...docTypeList.slice(index + 1)];
if (isArfDoc) {
setArfDocuments(updatedDocList);
if (arfInputRef.current) {
arfInputRef.current.files = toFileList(updatedDocList);
arfController.field.onChange(updatedDocList);
}
} else {
setLgDocuments(updatedDocList);
if (lgInputRef.current) {
lgInputRef.current.files = toFileList(updatedDocList);
lgController.field.onChange(updatedDocList);
}
const onRemove = (index: number, _docType: DOCUMENT_TYPE) => {
const updatedDocList: UploadDocument[] = [
...documents.slice(0, index),
...documents.slice(index + 1),
];

if (arfInputRef.current) {
arfInputRef.current.files = toFileList(updatedDocList);
arfController.field.onChange(updatedDocList);
}

const updatedFileList = [...mergeList, ...updatedDocList];
setDocuments(updatedFileList);
setDocuments(updatedDocList);
};

return (
<form
onSubmit={handleSubmit(submitDocuments)}
noValidate
data-testid="upload-document-form"
>
<Fieldset.Legend headingLevel="h1" isPageHeading>
Upload documents
</Fieldset.Legend>
<PatientSummary />

<Fieldset>
<h2>Electronic health records</h2>
<DocumentInputForm
showHelp
documents={arfDocuments}
onDocumentRemove={onRemove}
onDocumentInput={onInput}
formController={arfController}
inputRef={arfInputRef}
formType={DOCUMENT_TYPE.ARF}
/>
</Fieldset>
<Fieldset>
<h2>Lloyd George records</h2>
<DocumentInputForm
documents={lgDocuments}
onDocumentRemove={onRemove}
onDocumentInput={onInput}
formController={lgController}
inputRef={lgInputRef}
formType={DOCUMENT_TYPE.LLOYD_GEORGE}
/>
</Fieldset>
<Button
type="submit"
id="upload-button"
disabled={formState.isSubmitting || !hasFileInput}
<>
<BackButton />
<form
onSubmit={handleSubmit(submitDocuments)}
noValidate
data-testid="upload-document-form"
>
Upload
</Button>
</form>
<Fieldset.Legend headingLevel="h1" isPageHeading>
Upload documents
</Fieldset.Legend>
<PatientSummary />

<Fieldset>
<h2>Electronic health records</h2>
<DocumentInputForm
showHelp
documents={documents}
onDocumentRemove={onRemove}
onDocumentInput={onInput}
formController={arfController}
inputRef={arfInputRef}
formType={DOCUMENT_TYPE.ARF}
/>
</Fieldset>
<Button type="submit" id="upload-button" disabled={formState.isSubmitting}>
Upload
</Button>
</form>
</>
);
}

Expand Down
9 changes: 8 additions & 1 deletion app/src/components/blocks/testPanel/TestPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import TestToggle, { ToggleProps } from './TestToggle';

function TestPanel() {
const [config, setConfig] = useConfigContext();
const { isBsol, recordUploaded, userRole } = config.mockLocal;
const { isBsol, recordUploaded, userRole, patientIsActive } = config.mockLocal;

const updateLocalFlag = (key: keyof LocalFlags, value: boolean | REPOSITORY_ROLE) => {
setConfig({
Expand Down Expand Up @@ -58,6 +58,13 @@ function TestPanel() {
updateLocalFlag('recordUploaded', !recordUploaded);
},
},
'patient-active-toggle': {
label: 'Patient is active (turn off to visit ARF workflow)',
checked: !!patientIsActive,
onChange: () => {
updateLocalFlag('patientIsActive', !patientIsActive);
},
},
};

return (
Expand Down
9 changes: 6 additions & 3 deletions app/src/pages/patientSearchPage/PatientSearchPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import axios from 'axios';
import { routes } from '../../types/generic/routes';
import { REPOSITORY_ROLE, authorisedRoles } from '../../types/generic/authRole';
import useRole from '../../helpers/hooks/useRole';
import ConfigProvider from '../../providers/configProvider/ConfigProvider';
import { runAxeTest } from '../../helpers/test/axeTestHelper';

const mockedUseNavigate = jest.fn();
Expand Down Expand Up @@ -312,8 +313,10 @@ describe('PatientSearchPage', () => {
const renderPatientSearchPage = () => {
const patient: PatientDetails = buildPatientDetails();
render(
<PatientDetailsProvider patientDetails={patient}>
<PatientSearchPage />
</PatientDetailsProvider>,
<ConfigProvider>
<PatientDetailsProvider patientDetails={patient}>
<PatientSearchPage />
</PatientDetailsProvider>
</ConfigProvider>,
);
};
7 changes: 6 additions & 1 deletion app/src/pages/patientSearchPage/PatientSearchPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import useBaseAPIHeaders from '../../helpers/hooks/useBaseAPIHeaders';
import useBaseAPIUrl from '../../helpers/hooks/useBaseAPIUrl';
import { errorToParams } from '../../helpers/utils/errorToParams';
import useTitle from '../../helpers/hooks/useTitle';
import useConfig from '../../helpers/hooks/useConfig';

export const incorrectFormatMessage = "Enter patient's 10 digit NHS number";

Expand All @@ -27,6 +28,7 @@ function PatientSearchPage() {
const [submissionState, setSubmissionState] = useState<SEARCH_STATES>(SEARCH_STATES.IDLE);
const [statusCode, setStatusCode] = useState<null | number>(null);
const [inputError, setInputError] = useState<null | string>(null);
const { mockLocal } = useConfig();
const { register, handleSubmit } = useForm({
reValidateMode: 'onSubmit',
});
Expand Down Expand Up @@ -64,8 +66,11 @@ function PatientSearchPage() {
handleSuccess(patientDetails);
} catch (e) {
const error = e as AxiosError;
/* istanbul ignore if */
if (isMock(error)) {
handleSuccess(buildPatientDetails({ nhsNumber }));
handleSuccess(
buildPatientDetails({ nhsNumber, active: mockLocal.patientIsActive }),
);
return;
}
if (error.response?.status === 400) {
Expand Down
2 changes: 2 additions & 0 deletions app/src/providers/configProvider/ConfigProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type LocalFlags = {
isBsol?: boolean;
recordUploaded?: boolean;
userRole?: REPOSITORY_ROLE;
patientIsActive?: boolean;
};

export type GlobalConfig = {
Expand Down Expand Up @@ -43,6 +44,7 @@ const ConfigProvider = ({ children, configOverride, setConfigOverride }: Props)
? {
isBsol: true,
recordUploaded: true,
patientIsActive: true,
userRole: REPOSITORY_ROLE.GP_ADMIN,
}
: null;
Expand Down
1 change: 1 addition & 0 deletions lambdas/handlers/create_document_reference_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"DOCUMENT_STORE_DYNAMODB_NAME",
"LLOYD_GEORGE_DYNAMODB_NAME",
"STAGING_STORE_BUCKET_NAME",
"DOCUMENT_STORE_BUCKET_NAME",
]
)
@override_error_check
Expand Down
Loading
Loading