From a032b82efd8a44256de0733cad623d74d865ad04 Mon Sep 17 00:00:00 2001 From: Barrett Falk Date: Mon, 11 Dec 2023 09:09:42 -0800 Subject: [PATCH] feature/CE-32-Add-Attachments-When-Creating-New-Complaint (#223) Co-authored-by: afwilcox --- .../e2e/allegation-details-create.cy.ts | 12 +- .../cypress/e2e/complaint-attachments.cy.ts | 7 +- frontend/src/app/common/attachment-utils.ts | 65 +++++++ .../common/attachments-carousel.tsx | 18 +- .../details/complaint-details-create.tsx | 170 ++++++++++++------ .../details/complaint-details-edit.tsx | 32 ++-- 6 files changed, 217 insertions(+), 87 deletions(-) create mode 100644 frontend/src/app/common/attachment-utils.ts diff --git a/frontend/cypress/e2e/allegation-details-create.cy.ts b/frontend/cypress/e2e/allegation-details-create.cy.ts index 63bd6b324..40b07d16d 100644 --- a/frontend/cypress/e2e/allegation-details-create.cy.ts +++ b/frontend/cypress/e2e/allegation-details-create.cy.ts @@ -54,11 +54,11 @@ describe("Complaint Create Page spec - Create View", () => { cy.navigateToCreateScreen(); cy.selectItemById("complaint-type-select-id", "Enforcement"); - cy.get("#caller-name-id").clear().type(createCallerInformation.name); - cy.get("#complaint-address-id") + cy.get("#caller-name-id").click({ force: true }).clear().type(createCallerInformation.name); + cy.get("#complaint-address-id").click({ force: true }) .clear() .type(createCallerInformation.address); - cy.get("#complaint-email-id").clear().type(createCallerInformation.email); + cy.get("#complaint-email-id").click({ force: true }).clear().type(createCallerInformation.email); cy.get("#caller-primary-phone-id").click({ force: true }); cy.get("#caller-primary-phone-id").clear(); @@ -66,10 +66,10 @@ describe("Complaint Create Page spec - Create View", () => { createCallerInformation.phoneInput, ); - cy.get("#caller-info-secondary-phone-id") + cy.get("#caller-info-secondary-phone-id").click({ force: true }) .clear() .typeAndTriggerChange(createCallerInformation.secondaryInput); - cy.get("#caller-info-alternate-phone-id") + cy.get("#caller-info-alternate-phone-id").click({ force: true }) .clear() .typeAndTriggerChange(createCallerInformation.alternateInput); @@ -80,7 +80,7 @@ describe("Complaint Create Page spec - Create View", () => { cy.get("#complaint-location-description-textarea-id").click({ force: true, }); - cy.get("#complaint-location-description-textarea-id") + cy.get("#complaint-location-description-textarea-id").click({ force: true }) .clear() .type(createCallDetails.locationDescription, { delay: 0 }); cy.get("#complaint-description-textarea-id").click({ force: true }); diff --git a/frontend/cypress/e2e/complaint-attachments.cy.ts b/frontend/cypress/e2e/complaint-attachments.cy.ts index ccaaf049a..a44bc381d 100644 --- a/frontend/cypress/e2e/complaint-attachments.cy.ts +++ b/frontend/cypress/e2e/complaint-attachments.cy.ts @@ -47,7 +47,7 @@ describe("Complaint Attachments", () => { }); Cypress._.times(complaintTypes.length, (index) => { - it("Verifies that upload option exists ", () => { + it("Verifies that upload option exists on edit page", () => { if ("#hwcr-tab".includes(complaintTypes[index])) { cy.navigateToEditScreen(COMPLAINT_TYPES.HWCR, "23-000076"); } else { @@ -60,4 +60,9 @@ describe("Complaint Attachments", () => { }); }); + + it("Verifies that upload option exists on the create page", () => { + cy.navigateToCreateScreen(); + cy.get("button.coms-carousel-upload-container").should("exist"); + }); }); diff --git a/frontend/src/app/common/attachment-utils.ts b/frontend/src/app/common/attachment-utils.ts new file mode 100644 index 000000000..012c12075 --- /dev/null +++ b/frontend/src/app/common/attachment-utils.ts @@ -0,0 +1,65 @@ +import { + deleteAttachments, + getAttachments, + saveAttachments, +} from "../store/reducers/attachments"; +import { COMSObject } from "../types/coms/object"; + +// used to update the state of attachments that are to be added to a complaint +export const handleAddAttachments = ( + setAttachmentsToAdd: React.Dispatch>, + selectedFiles: File[] +) => { + setAttachmentsToAdd((prevFiles) => + prevFiles ? [...prevFiles, ...selectedFiles] : selectedFiles + ); +}; + +// used to update the state of attachments that are to be deleted from a complaint +export const handleDeleteAttachments = ( + attachmentsToAdd: File[] | null, + setAttachmentsToAdd: React.Dispatch>, + setAttachmentsToDelete: React.Dispatch< + React.SetStateAction + >, + fileToDelete: COMSObject +) => { + if (!fileToDelete.pendingUpload) { + // a user is wanting to delete a previously uploaded attachment + setAttachmentsToDelete((prevFiles) => + prevFiles ? [...prevFiles, fileToDelete] : [fileToDelete] + ); + } else if (attachmentsToAdd) { + // a user has added an attachment and deleted it, before the complaint was saved. Let's make sure this file isn't uploaded, so remove it from the "attachmentsToAdd" state + setAttachmentsToAdd((prevAttachments) => + prevAttachments + ? prevAttachments.filter((file) => decodeURIComponent(file.name) !== decodeURIComponent(fileToDelete.name)) + : null + ); + } +}; + +// Given a list of attachments to add/delete, call COMS to add/delete those attachments +export async function handlePersistAttachments( + dispatch: any, + attachmentsToAdd: File[] | null, + attachmentsToDelete: COMSObject[] | null, + complaintIdentifier: string, + setAttachmentsToAdd: any, + setAttachmentsToDelete: any +) { + if (attachmentsToDelete) { + await dispatch(deleteAttachments(attachmentsToDelete)); + } + + if (attachmentsToAdd) { + await dispatch(saveAttachments(attachmentsToAdd, complaintIdentifier)); + } + + // refresh store + await dispatch(getAttachments(complaintIdentifier)); + + // Clear the attachments since they've been added or saved. + setAttachmentsToAdd(null); + setAttachmentsToDelete(null); +} diff --git a/frontend/src/app/components/common/attachments-carousel.tsx b/frontend/src/app/components/common/attachments-carousel.tsx index 4119c1b67..81f349fba 100644 --- a/frontend/src/app/components/common/attachments-carousel.tsx +++ b/frontend/src/app/components/common/attachments-carousel.tsx @@ -21,7 +21,7 @@ import { selectMaxFileSize } from "../../store/reducers/app"; import { v4 as uuidv4 } from 'uuid'; type Props = { - complaintIdentifier: string; + complaintIdentifier?: string; allowUpload?: boolean; allowDelete?: boolean; onFilesSelected?: (attachments: File[]) => void; @@ -53,7 +53,7 @@ export const AttachmentsCarousel: FC = ({ // when the carousel data updates (from the selector, on load), populate the carousel slides useEffect(() => { if (carouselData) { - setSlides(carouselData); + setSlides(sortAttachmentsByName(carouselData)); } else { setSlides([]) } @@ -61,7 +61,9 @@ export const AttachmentsCarousel: FC = ({ // get the attachments when the complaint loads useEffect(() => { - dispatch(getAttachments(complaintIdentifier)); + if (complaintIdentifier) { + dispatch(getAttachments(complaintIdentifier)); + } }, [complaintIdentifier, dispatch]); //-- when the component unmounts clear the attachments from redux @@ -71,6 +73,16 @@ export const AttachmentsCarousel: FC = ({ }; }, [dispatch]); + function sortAttachmentsByName(comsObjects: COMSObject[]): COMSObject[] { + // Create a copy of the array using slice() or spread syntax + const copy = [...comsObjects]; + + // Sort the copy based on the name property + copy.sort((a, b) => a.name.localeCompare(b.name)); + + return copy; +} + // when a user selects files (via the file browser that pops up when clicking the upload slide) then add them to the carousel const onFileSelect = (newFiles: FileList) => { const selectedFilesArray = Array.from(newFiles); diff --git a/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx b/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx index 39a4b306d..7c9457bbc 100644 --- a/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx +++ b/frontend/src/app/components/containers/complaints/details/complaint-details-create.tsx @@ -46,6 +46,9 @@ import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import { useNavigate } from "react-router-dom"; import { ComplaintLocation } from "./complaint-location"; +import { AttachmentsCarousel } from "../../../common/attachments-carousel"; +import { COMSObject } from "../../../../types/coms/object"; +import { handleAddAttachments, handleDeleteAttachments, handlePersistAttachments } from "../../../../common/attachment-utils"; export const CreateComplaint: FC = () => { const dispatch = useAppDispatch(); @@ -225,6 +228,21 @@ export const CreateComplaint: FC = () => { HwcrComplaint | AllegationComplaint >(newEmptyComplaint); + // files to add to COMS when complaint is saved + const [attachmentsToAdd, setAttachmentsToAdd] = useState(null); + + // files to remove from COMS when complaint is saved + const [attachmentsToDelete, setAttachmentsToDelete] = useState(null); + + const onHandleAddAttachments = (selectedFiles: File[]) => { + handleAddAttachments(setAttachmentsToAdd, selectedFiles); + }; + + const onHandleDeleteAttachment = (fileToDelete: COMSObject) => { + handleDeleteAttachments(attachmentsToAdd, setAttachmentsToAdd, setAttachmentsToDelete, fileToDelete); + }; + + function noErrors() { let noErrors = false; if ( @@ -955,7 +973,20 @@ export const CreateComplaint: FC = () => { if (!createComplaint) { return; } + let complaint = createComplaint; + setComplaintToOpenStatus(complaint); + + const noError = await setErrors(complaint); + + if (noError && noErrors()) { + await handleComplaintProcessing(complaint); + } else { + handleFormErrors(); + } + }; + + const setComplaintToOpenStatus = (complaint: HwcrComplaint | AllegationComplaint) => { const openStatus = { short_description: "OPEN", long_description: "Open", @@ -967,63 +998,86 @@ export const CreateComplaint: FC = () => { update_user_id: "", update_utc_timestamp: null, }; - - complaint.complaint_identifier.complaint_status_code = openStatus; //force OPEN - - const noError = await setErrors(complaint); - - if (noError && noErrors()) { - complaint.complaint_identifier.create_utc_timestamp = - complaint.complaint_identifier.update_utc_timestamp = - new Date().toDateString(); - complaint.complaint_identifier.create_user_id = - complaint.complaint_identifier.update_user_id = userid; - complaint.complaint_identifier.location_geometry_point.type = "Point"; - if ( - complaint.complaint_identifier.location_geometry_point.coordinates - .length === 0 - ) { - complaint.complaint_identifier.location_geometry_point.coordinates = [ - 0, 0, - ]; - } - setCreateComplaint(complaint); - if (complaintType === COMPLAINT_TYPES.HWCR) { - const complaintId = await dispatch( - createWildlifeComplaint(complaint as HwcrComplaint), - ); - if (complaintId) { - await dispatch( - getWildlifeComplaintByComplaintIdentifierSetUpdate( - complaintId, - setCreateComplaint, - ), - ); - - navigate("/complaint/" + complaintType + "/" + complaintId); - } - } - else if (complaintType === COMPLAINT_TYPES.ERS) { - const complaintId = await dispatch( - createAllegationComplaint(complaint as AllegationComplaint), - ); - if (complaintId) { - await dispatch( - getAllegationComplaintByComplaintIdentifierSetUpdate( - complaintId, - setCreateComplaint, - ), - ); - - navigate("/complaint/" + complaintType + "/" + complaintId); - } - } - setErrorNotificationClass("comp-complaint-error display-none"); - } else { - ToggleError("Errors in form"); - setErrorNotificationClass("comp-complaint-error"); + complaint.complaint_identifier.complaint_status_code = openStatus; + }; + + const handleComplaintProcessing = async (complaint: HwcrComplaint | AllegationComplaint) => { + updateComplaintDetails(complaint); + setCreateComplaint(complaint); + + let complaintId = await processComplaintBasedOnType(complaint); + if (complaintId) { + handlePersistAttachments(dispatch, attachmentsToAdd, attachmentsToDelete, complaintId, setAttachmentsToAdd, setAttachmentsToDelete); } + + setErrorNotificationClass("comp-complaint-error display-none"); }; + + const updateComplaintDetails = (complaint: HwcrComplaint | AllegationComplaint) => { + const now = new Date().toDateString(); + complaint.complaint_identifier.create_utc_timestamp = now; + complaint.complaint_identifier.update_utc_timestamp = now; + complaint.complaint_identifier.create_user_id = userid; + complaint.complaint_identifier.update_user_id = userid; + complaint.complaint_identifier.location_geometry_point.type = "Point"; + + if ( + complaint.complaint_identifier.location_geometry_point.coordinates.length === 0 + ) { + complaint.complaint_identifier.location_geometry_point.coordinates = [0, 0]; + } + }; + + const processComplaintBasedOnType = async (complaint: HwcrComplaint | AllegationComplaint) => { + switch (complaintType) { + case COMPLAINT_TYPES.HWCR: + return handleHwcrComplaint(complaint); + case COMPLAINT_TYPES.ERS: + return handleErsComplaint(complaint); + default: + return null; + } + }; + + const handleHwcrComplaint = async (complaint: HwcrComplaint | AllegationComplaint) => { + const complaintId = await dispatch( + createWildlifeComplaint(complaint as HwcrComplaint) + ); + if (complaintId) { + await dispatch( + getWildlifeComplaintByComplaintIdentifierSetUpdate( + complaintId, + setCreateComplaint + ) + ); + + navigate("/complaint/" + complaintType + "/" + complaintId); + } + return complaintId; + }; + + const handleErsComplaint = async (complaint: HwcrComplaint | AllegationComplaint) => { + const complaintId = await dispatch( + createAllegationComplaint(complaint as AllegationComplaint) + ); + if (complaintId) { + await dispatch( + getAllegationComplaintByComplaintIdentifierSetUpdate( + complaintId, + setCreateComplaint + ) + ); + + navigate("/complaint/" + complaintType + "/" + complaintId); + } + return complaintId; + }; + + const handleFormErrors = () => { + ToggleError("Errors in form"); + setErrorNotificationClass("comp-complaint-error"); + }; + const maxDate = new Date(); @@ -1573,6 +1627,12 @@ export const CreateComplaint: FC = () => { )} + ); }; diff --git a/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx b/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx index bec15ff60..a179f3036 100644 --- a/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx +++ b/frontend/src/app/components/containers/complaints/details/complaint-details-edit.tsx @@ -60,8 +60,8 @@ import { CallDetails } from "./call-details"; import { CallerInformation } from "./caller-information"; import { SuspectWitnessDetails } from "./suspect-witness-details"; import { AttachmentsCarousel } from "../../../common/attachments-carousel"; -import { deleteAttachments, saveAttachments } from "../../../../store/reducers/attachments"; import { COMSObject } from "../../../../types/coms/object"; +import { handleAddAttachments, handleDeleteAttachments, handlePersistAttachments } from "../../../../common/attachment-utils"; type ComplaintParams = { id: string; @@ -132,19 +132,14 @@ export const ComplaintDetailsEdit: FC = () => { // files to remove from COMS when complaint is saved const [attachmentsToDelete, setAttachmentsToDelete] = useState(null); - const handleAddAttachments = (selectedFiles: File[]) => { - setAttachmentsToAdd(prevFiles => prevFiles ? [...prevFiles, ...selectedFiles] : selectedFiles); + const onHandleAddAttachments = (selectedFiles: File[]) => { + handleAddAttachments(setAttachmentsToAdd, selectedFiles); }; - const handleDeleteAttachment = (fileToDelete: COMSObject) => { - if (!fileToDelete.pendingUpload) { - setAttachmentsToDelete(prevFiles => prevFiles ? [...prevFiles, fileToDelete] : [fileToDelete]); - } else if (attachmentsToAdd) { // we're deleting an attachment that wasn't uploaded, so remove the attachment from the "attachmentsToDelete" state - setAttachmentsToAdd(prevAttachments => prevAttachments ? prevAttachments.filter(file => file.name !== fileToDelete.name) : null); - } + const onHandleDeleteAttachment = (fileToDelete: COMSObject) => { + handleDeleteAttachments(attachmentsToAdd, setAttachmentsToAdd, setAttachmentsToDelete, fileToDelete); }; - const [errorNotificationClass, setErrorNotificationClass] = useState( "comp-complaint-error display-none" ); @@ -174,21 +169,14 @@ export const ComplaintDetailsEdit: FC = () => { } setErrorNotificationClass("comp-complaint-error display-none"); setReadOnly(true); + + handlePersistAttachments(dispatch, attachmentsToAdd, attachmentsToDelete, id, setAttachmentsToAdd, setAttachmentsToDelete); + } else { ToggleError("Errors in form"); setErrorNotificationClass("comp-complaint-error"); }; - if (attachmentsToAdd) { - dispatch(saveAttachments(attachmentsToAdd, id)); - } - if (attachmentsToDelete) { - dispatch(deleteAttachments(attachmentsToDelete)) - } - - // clear the attachments since they've been added or saved. If they couldn't be added or saved then an error would have appeared - setAttachmentsToAdd(null); - setAttachmentsToDelete(null); }; useEffect(() => { @@ -1659,8 +1647,8 @@ export const ComplaintDetailsEdit: FC = () => { complaintIdentifier={id} allowUpload={true} allowDelete={true} - onFilesSelected={handleAddAttachments} - onFileDeleted={handleDeleteAttachment} + onFilesSelected={onHandleAddAttachments} + onFileDeleted={onHandleDeleteAttachment} />