Skip to content

Commit

Permalink
feature/CE-104 upload an attachment to an existing complaint (#210)
Browse files Browse the repository at this point in the history
Co-authored-by: afwilcox <[email protected]>
  • Loading branch information
barrfalk and afwilcox authored Dec 6, 2023
1 parent 6a1c704 commit 00babbf
Show file tree
Hide file tree
Showing 18 changed files with 617 additions and 117 deletions.
19 changes: 19 additions & 0 deletions backend/db/migrations/R__Configuration-values.sql
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,23 @@ true,
CURRENT_USER,
CURRENT_TIMESTAMP,
CURRENT_USER,
CURRENT_TIMESTAMP) ON CONFLICT DO NOTHING;

insert
into
configuration(configuration_code,
configuration_value,
long_description,
active_ind,
create_user_id,
create_utc_timestamp,
update_user_id,
update_utc_timestamp)
values ('MAXFILESZ',
'5000',
'The maximum file size (in Megabytes) supported for upload.',
true,
CURRENT_USER,
CURRENT_TIMESTAMP,
CURRENT_USER,
CURRENT_TIMESTAMP) ON CONFLICT DO NOTHING;
18 changes: 17 additions & 1 deletion frontend/cypress/e2e/complaint-attachments.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,26 @@ describe("Complaint Attachments", () => {
.should('exist')
.and('not.be.visible');

// should not be able to upload on details view
cy.get("button.coms-carousel-upload-container").should("not.exist");

cy.get(".coms-carousel-actions").first().invoke('attr', 'style', 'display: block');

// cypress can't verify things that happen in other tabs, so don't open attachments in another tab
cy.get(".download-icon").should("exist");
});
});

Cypress._.times(complaintTypes.length, (index) => {
it("Verifies that upload option exists ", () => {
if ("#hwcr-tab".includes(complaintTypes[index])) {
cy.navigateToEditScreen(COMPLAINT_TYPES.HWCR, "23-000076");
} else {
cy.navigateToEditScreen(COMPLAINT_TYPES.ERS, "23-006888");
}

// should be able to upload on details view
cy.get("button.coms-carousel-upload-container").should("exist");

});

Expand Down
34 changes: 31 additions & 3 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
"@types/react-phone-number-input": "^3.0.14",
"@types/react-transition-group": "^4.4.6",
"@types/redux-persist": "^4.3.1",
"@types/uuid": "^9.0.7",
"@types/warning": "^3.0.0",
"@typescript-eslint/parser": "5.56.0",
"axios": "^1.4.0",
Expand Down Expand Up @@ -82,6 +83,7 @@
"typescript": "*",
"uncontrollable": "^8.0.2",
"urlpattern-polyfill": "^9.0.0",
"uuid": "^9.0.1",
"warning": "^4.0.3",
"web-vitals": "^3.0.0"
},
Expand Down
81 changes: 81 additions & 0 deletions frontend/src/app/common/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,46 @@ export const get = <T, M = {}>(
});
};

export const deleteMethod = <T, M = {}>(
dispatch: Dispatch,
parameters: ApiRequestParameters<M>,
headers?: {}
): Promise<T> => {
let config: AxiosRequestConfig = { headers: headers };
return new Promise<T>((resolve, reject) => {
const { url, requiresAuthentication, params } = parameters;

if (requiresAuthentication) {
axios.defaults.headers.common[
"Authorization"
] = `Bearer ${localStorage.getItem(AUTH_TOKEN)}`;
}

if (params) {
config.params = params;
}

axios
.delete(url, config)
.then((response: AxiosResponse) => {
const { data, status } = response;

if (status === STATUS_CODES.Unauthorized) {
window.location = KEYCLOAK_URL;
}

resolve(data as T);
})
.catch((error: AxiosError) => {
if (parameters.enableNotification) {
const { message } = error;
dispatch(toggleNotification("error", message));
}
reject(error);
});
});
};

export const post = <T, M = {}>(
dispatch: Dispatch,
parameters: ApiRequestParameters<M>,
Expand Down Expand Up @@ -205,3 +245,44 @@ export const put = <T, M = {}>(
});
});
};

export const putFile = <T, M = {}>(
dispatch: Dispatch,
parameters: ApiRequestParameters<M>,
headers: {},
file: File,
): Promise<T> => {
let config: AxiosRequestConfig = { headers: headers };

const formData = new FormData();
if (file)
formData.append('file', file);

return new Promise<T>((resolve, reject) => {
const { url, requiresAuthentication } = parameters;

if (requiresAuthentication) {
axios.defaults.headers.common[
"Authorization"
] = `Bearer ${localStorage.getItem(AUTH_TOKEN)}`;
}

axios
.put(url, file, config)
.then((response: AxiosResponse) => {
const { status } = response;

if (status === STATUS_CODES.Unauthorized) {
window.location = KEYCLOAK_URL;
}

resolve(response.data as T);
})
.catch((error: AxiosError) => {
if (parameters.enableNotification) {
dispatch(toggleNotification("error", error.message));
}
reject(error);
});
});
};
27 changes: 27 additions & 0 deletions frontend/src/app/common/methods.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,23 @@ export const formatDateTime = (input: string | undefined): string => {
return format(Date.parse(input), "yyyy-MM-dd HH:mm:ss");
};

// given a filename and complaint identifier, inject the complaint identifier inbetween the file name and extension
export const injectComplaintIdentifierToFilename = (filename: string, complaintIdentifier: string): string => {
// Find the last dot in the filename to separate the extension
const lastDotIndex = filename.lastIndexOf('.');

// If there's no dot, just append the complaintId at the end
if (lastDotIndex === -1) {
return (`${filename} ${complaintIdentifier}`);
}

const fileNameWithoutExtension = filename.substring(0, lastDotIndex);
const fileExtension = filename.substring(lastDotIndex);

// Otherwise, insert the complaintId before the extension
return (`${fileNameWithoutExtension} ${complaintIdentifier}${fileExtension}`);
}

// Used to retrieve the coordinates in the decimal format
export const parseDecimalDegreesCoordinates = (
coordinates: Coordinate,
Expand Down Expand Up @@ -154,3 +171,13 @@ export const truncateString = (str: string, maxLength: number): string=> {
return str;
}
}

export const removeFile = (fileList: FileList, fileToRemove: File): File[] => {
// Convert the FileList to an array
const filesArray = Array.from(fileList);

// Filter out the file you want to remove
const updatedFilesArray = filesArray.filter(file => file !== fileToRemove);

return updatedFilesArray;
}
31 changes: 26 additions & 5 deletions frontend/src/app/components/common/attachment-slide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ import AttachmentIcon from "./attachment-icon";
type Props = {
index: number;
attachment: COMSObject;
onFileRemove: (attachment: COMSObject) => void;
allowDelete?: boolean;
};

export const AttachmentSlide: FC<Props> = ({
index,
attachment,
allowDelete,
onFileRemove,
}) => {
const dispatch = useAppDispatch();

Expand All @@ -39,27 +41,46 @@ export const AttachmentSlide: FC<Props> = ({
a.click();
};

const getSlideClass = () => {
let className = "";

if (attachment.errorMesage) {
className = "error-slide";
} else if (attachment.pendingUpload) {
className = "pending-slide";
}

return className;
}

return (
<Slide index={index} key={index}>
<div className="coms-carousel-slide">
<div className={`coms-carousel-slide ${getSlideClass()}`}>
<div className="coms-carousel-actions">
{allowDelete && <BsTrash className="delete-icon" tabIndex={index} />}
{!attachment.pendingUpload && (
<BsCloudDownload
tabIndex={index}
className="download-icon"
onClick={() =>
handleAttachmentClick(`${attachment.id}`, `${attachment.name}`)
}
/>
/>)}
{allowDelete && <BsTrash className="delete-icon" tabIndex={index} onClick={() => onFileRemove(attachment)} />}
</div>
<div className="top-section">
<AttachmentIcon filename={attachment.name}/>
</div>
<div className="bottom-section">
<div className="slide_text slide_file_name">{attachment.name}</div>
<div className="slide_text slide_file_name" >{decodeURIComponent(attachment.name)}</div>
{attachment?.pendingUpload && attachment?.errorMesage ? (
<div>
{attachment?.errorMesage}
</div>
) : (
<div className="slide_text">
{formatDateTime(attachment.createdAt.toString())}
{attachment?.pendingUpload ? 'Pending upload...' : formatDateTime(attachment.createdAt?.toString())}
</div>
)}
</div>
</div>
</Slide>
Expand Down
53 changes: 53 additions & 0 deletions frontend/src/app/components/common/attachment-upload.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { FC, useRef } from "react";
import { BsPlus } from "react-icons/bs";

type Props = {
onFileSelect: (selectedFile: FileList) => void;
};

export const AttachmentUpload: FC<Props> = ({
onFileSelect,
}) => {
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
onFileSelect(event.target.files);
}
};

// Without this, I'm unable to re-add the same file twice.
const handleFileClick = () => {
// Clear the current file input value
if (fileInputRef.current) {
fileInputRef.current.value = "";
}
};

const fileInputRef = useRef<HTMLInputElement>(null);

const handleDivClick = () => {
fileInputRef.current?.click();
};

return (
<div>
<input
type="file"
multiple
onChange={handleFileChange}
onClick={handleFileClick}
ref={fileInputRef}
style={{ display: "none" }}
/>
<button
className="coms-carousel-upload-container"
tabIndex={0}
onClick={handleDivClick}
>
<div className="upload-icon">
<BsPlus />
</div>
<div className="upload-text">Upload</div>
</button>
</div>
);
};
Loading

0 comments on commit 00babbf

Please sign in to comment.