Skip to content

Commit

Permalink
WIP for #1676
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Mar 7, 2025
1 parent 04d458c commit 9e4978c
Show file tree
Hide file tree
Showing 14 changed files with 374 additions and 108 deletions.
22 changes: 14 additions & 8 deletions client/src/__tests__/utils/CSVParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ coShortName,admin,[email protected], , , , ,
`;
const results = parseBulkInvitation(csv);
expect(results.error).toEqual(false);

const data = results.data;
expect(data.length).toEqual(3);
expect(results.errors.length).toEqual(0);
expect(data[0].invitation_expiry_date).toEqual(1743014227);
expect(data[0].membership_expiry_date).toEqual(1767052800);
expect(data[1].short_names).toEqual(["cumulusgrp","cirrusgrp"]);
Expand All @@ -30,8 +29,6 @@ cumulusgrp,admin,[email protected],301ee8e6-b5d1-40b5-a27e-47611f803371,"202
`;
const results = parseBulkInvitation(csv);
expect(results.error).toEqual(false);

const data = results.data;
expect(data.length).toEqual(2);
expect(data[1].short_names).toEqual(["cumulusgrp","cirrusgrp"]);
Expand All @@ -43,10 +40,19 @@ test("parseWithError", () => {
Mumbo Jumbo
`;
const results = parseBulkInvitation(csv);
expect(results.data.length).toEqual(0);
expect(results.error).toEqual(true);

expect(results.data.length).toEqual(1);
const errors = results.errors;
expect(errors.length).toEqual(2);
expect(errors.length).toEqual(1);
expect(errors[0].code).toEqual("TooFewFields");
});

test("parseWithCustomErrors", () => {
const csv = `
short_names,intended_role,invitees,groups,invitation_expiry_date,membership_expiry_date,message,sender_name
cumulusgrp,admin,[email protected],301ee8e6-b5d1-40b5-a27e-47611f803371,1743014227,2025-12-30,Please join the Cumulus research group collaboration page.,Organisation XYZ
,,,301ee8e6-b5d1-40b5-a27e-47611f803371,1743014227174.00,1743014227174.00,Please join the Cumulus research group collaboration page.,Organisation XYZ
`;
const results = parseBulkInvitation(csv);
expect(results.data.length).toEqual(2);
expect(results.errors.length).toEqual(1);
});
3 changes: 3 additions & 0 deletions client/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,9 @@ export function invitationExists(emails, collaborationId) {
return postPutJson("/api/invitations/exists_email", body, "POST");
}

export function invitationBulkUpload(data) {
return postPutJson("/api/invitations/bulk_upload", data, "PUT");
}

//Organisation Memberships
export function deleteOrganisationMembership(organisationId, userId, showErrorDialog = true) {
Expand Down
69 changes: 69 additions & 0 deletions client/src/components/TabularData.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React, {Fragment} from "react";
import "./TabularData.scss";
import I18n from "../locale/I18n";
import DOMPurify from "dompurify";
import {isEmpty} from "../utils/Utils";
import {dateColumns, requiredColumns} from "../utils/CSVParser";

export default function TabularData({headers = [], data = [], errors = [], showRequiredInfo = true}) {

const errorTranslation = (row, errorCode) => {
const hasErrorTranslation = I18n.translations[I18n.locale].bulkUpload.errors[errorCode];
return I18n.t(`bulkUpload.errors.${hasErrorTranslation ? errorCode : "unknown"}`, {
fields: requiredColumns.filter(field => isEmpty(row[field])).join(", ")
});
}

const displayHeader = header => {
if (requiredColumns.includes(header)) {
return header + "<sup class='required'>*</sup>";
}
return header;
}

const displayValue = (header, value) => {
if (Array.isArray(value)) {
return value.join(", ");
}
if (dateColumns.includes(header) && !isEmpty(value)) {
const isoString = new Date(parseInt(value, 10) * 1000).toISOString();
return isoString.substring(0, 10);
}
return value || "";
}

return (
<div className="tabular-data">
<table>
<thead>
<tr>
{headers.map((header, index) =>
<th key={index}
dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(displayHeader(header))}}/>
)}
</tr>
</thead>
<tbody>
{data.map((row, index) => <Fragment key={index}>
<tr>
{headers.map((header, innerIndex) =>
<td key={innerIndex}
dangerouslySetInnerHTML={{__html: DOMPurify.sanitize(displayValue(header, row[header]))}}/>
)}
</tr>
{errors.some(error => error.row === index) &&
<tr>
<td className="error" colSpan={headers.length}>
{errorTranslation(row, errors.find(error => error.row === index).code)}
</td>
</tr>}
</Fragment>)}
</tbody>
</table>
{showRequiredInfo &&
<p className="info"><sup className='required'>*</sup><em>{I18n.t("bulkUpload.requiredInfo")}</em></p>}
</div>
);

}

58 changes: 58 additions & 0 deletions client/src/components/TabularData.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
@import "../stylesheets/vars.scss";

.tabular-data {
margin-bottom: 25px;

table {
width: 100%;
border-collapse: collapse;
font-family: Arial, sans-serif;
font-size: 14px;
}

th, td {
border: 1px solid #d4d4d4;
padding: 8px;
text-align: left;
}

th {
background-color: #f3f3f3;
font-weight: bold;
}

tr:nth-child(even) {
background-color: #f9f9f9;
}

tr:hover {
background-color: #e6f7ff;
}

td.error {
color: white;
background-color: var(--sds--color--red--400);

&:hover {
background-color: var(--sds--color--red--500);
}
}

input {
border: none;
width: 100%;
padding: 4px;
font-size: 14px;
background: transparent;
}

input:focus {
outline: 2px solid #4a90e2;
background-color: #fff;
}

p.info {
margin-top: 8px;
font-size: 14px;
}
}
20 changes: 13 additions & 7 deletions client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -2744,18 +2744,24 @@ const en = {
breadcrumb: "bulk-upload",
main: "Upload",
docs: "Documentation",
dragDrop: "Drag and drop CSV file or",
click: " click to upload",
dragDrop: "Drag and drop a CSV file or",
click: " click here to upload",
errorWrongExtension: "Only CSV files can be uploaded, not {{name}}",
errorFormat: "Error parsing file {{name}}",
successFullyParsed: "Successfully parsed CSV<br/> <strong>{{invitees}}</strong> invitees will be invited for <strong>{{collaborations}}</strong> collaborations in <strong>{{groups}}</strong> groups.",
errorParsed: "Error in parsing CSV. See the errors below",
successFullyParsed: "Successfully parsed {{fileName}}<br/> <strong>{{invitees}}</strong> invitees will be invited for <strong>{{collaborations}}</strong> collaborations in <strong>{{groups}}</strong> groups.",
errorParsed: "Error in parsing {{fileName}}. See the details below",
errorRows: "However there are some rows that will be excluded, because of missing required fields. See details below.",
showDetails: "Show details",
hideDetails: "Hide details",
schema: "Click the button below to download a sample CVS file. You can also see the CSV schema file for individual column requirements.",
schema: "Click the button below to download a sample CVS file.",
download: "Download",
showSchema: "Show schema",
hideSchema: "Hide schema",
exampleInfo: "Example CSV data",
requiredInfo: "indicates a required value",
proceed: "Upload & send invitations",
errors: {
TooFewFields: "The row above has missing values for required fields: {{fields}}",
unknown: "Invalid CSV row"
}
}
};

Expand Down
20 changes: 13 additions & 7 deletions client/src/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -2744,18 +2744,24 @@ const nl = {
breadcrumb: "bulk-upload",
main: "Upload",
docs: "Documentation",
dragDrop: "Drag and drop CSV file or",
click: " click to upload",
dragDrop: "Drag and drop a CSV file or",
click: " click here to upload",
errorWrongExtension: "Only CSV files can be uploaded, not {{name}}",
errorFormat: "Error parsing file {{name}}",
successFullyParsed: "Successfully parsed CSV<br/> <strong>{{invitees}}</strong> invitees will be invited for <strong>{{collaborations}}</strong> collaborations in <strong>{{groups}}</strong> groups.",
errorParsed: "Error in parsing CSV. See the errors below",
successFullyParsed: "Successfully parsed {{fileName}}<br/> <strong>{{invitees}}</strong> invitees will be invited for <strong>{{collaborations}}</strong> collaborations in <strong>{{groups}}</strong> groups.",
errorParsed: "Error in parsing {{fileName}}. See the details below",
errorRows: "However there are some rows that will be excluded, because of missing required fields. See details below.",
showDetails: "Show details",
hideDetails: "Hide details",
schema: "Click the button below to download a sample CVS file. You can also see the CSV schema file for individual column requirements.",
schema: "Click the button below to download a sample CVS file.",
download: "Download",
showSchema: "Show schema",
hideSchema: "Hide schema",
exampleInfo: "Example CSV data",
requiredInfo: "indicates a required value",
proceed: "Upload & send invitations",
errors: {
TooFewFields: "The row above has missing values for required fields: {{fields}}",
unknown: "Invalid CSV row"
}
}
};

Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,7 @@ class App extends React.Component {
refreshUser={this.refreshUserMemberships}
{...props}/>}/>

<Route path="/bulk-upload"
<Route path="/bulk-upload/:tab?"
render={props =>
<ProtectedRoute
currentUser={currentUser}
Expand Down
Loading

0 comments on commit 9e4978c

Please sign in to comment.