Skip to content

Commit

Permalink
Review and testing
Browse files Browse the repository at this point in the history
  • Loading branch information
agreenspan24 committed Jun 22, 2021
1 parent 2928512 commit d4695dc
Show file tree
Hide file tree
Showing 4 changed files with 169 additions and 95 deletions.
77 changes: 76 additions & 1 deletion __test__/lib/parse-csv.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Papa from "papaparse";
import { parseCSV, organizationCustomFields } from "../../src/lib";
import { parseCSV, organizationCustomFields, parseCannedResponseCsv } from "../../src/lib";

describe("parseCSV", () => {
describe("with PHONE_NUMBER_COUNTRY set", () => {
Expand Down Expand Up @@ -345,3 +345,78 @@ describe("parseCSV", () => {
});
});
});

describe("parseCannedResponseCsv", () => {
const tags = [
{
id: 1,
name: "Tag1",
description: "Tag1Desc"
},
{
id: 2,
name: "Tag2",
description: "Tag2Desc"
}
];

it("fails when title header doesn't exist", () => {
const csv = "text,text,tags\nhello,world,tag1";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBe("Missing columns: Title");
expect(cannedResponses).toBeFalsy();
});
});

it("fails when text header doesn't exist", () => {
const csv = "title,title,tags\nhello,world,tag1";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBe("Missing columns: Text");
expect(cannedResponses).toBeFalsy();
});
});

it("fails when title and text header doesn't exist", () => {
const csv = "tags,tags,tags\nhello,world,tag1";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBe("Missing columns: Title, Text");
expect(cannedResponses).toBeFalsy();
});
});

it("fails when row is missing title", () => {
const csv = "title,text,tags\n,world,tag1";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBe("Incomplete Line. Title: ; Text: world");
expect(cannedResponses).toBeFalsy();
});
});


it("fails when row is missing text", () => {
const csv = "title,text,tags\nhello,,tag1";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBe("Incomplete Line. Title: hello; Text: ");
expect(cannedResponses).toBeFalsy();
});
});

it("fails when tag cannot be matched", () => {
const csv = "title,text,tags\nhello,world,tag3";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBe(`"tag3" cannot be found in your organization's tags`);
expect(cannedResponses).toBeFalsy();
});
});

it("succeeds with complete data", () => {
const csv = "title,text,tags\nhello,world,\nhi,there,tag1";
parseCannedResponseCsv(csv, tags, ({ error, cannedResponses }) => {
expect(error).toBeFalsy();
expect(cannedResponses).toEqual([
{ title: 'hello', text: 'world', tagIds: [] },
{ title: 'hi', text: 'there', tagIds: [1] }
]);
});
});
})
117 changes: 55 additions & 62 deletions src/components/CampaignCannedResponsesForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,54 +95,9 @@ export class CampaignCannedResponsesForm extends React.Component {
.replace(/[^a-zA-Z1-9]+/g, "")
}

showUploadCsvButton() {
showAddButton(cannedResponses) {
this.uploadCsvInputRef = React.createRef();

return (
<div>
<div className={css(styles.flexEnd)}>
<Tooltip
title="Upload a CSV of canned responses with columns for Title, Text, and Tags"
>
<IconButton
onClick={() => this.uploadCsvInputRef.current.click()}
disabled={this.state.uploadingCsv}
>
<PublishIcon />
</IconButton>
</Tooltip>
{this.props.formValues.cannedResponses.length > 0 ? (
<Tooltip
title="Remove all Canned Responses"
>
<IconButton
onClick={() => this.props.onChange({
cannedResponses: []
})}
>
<ClearIcon />
</IconButton>
</Tooltip>
) : ""}
</div>
<input
type="file"
accept=".csv"
ref={this.uploadCsvInputRef}
onChange={this.handleCsvUpload}
onClick={e => (e.target.value = null)}
style={{ display: "none" }}
/>
{this.state.uploadCsvError && (
<div className={css(styles.redText)}>
{this.state.uploadCsvError}
</div>
)}
</div>
);
}

showAddButton() {
if (!this.state.showForm) {
return (
<div className={css(styles.spaceBetween)}>
Expand All @@ -159,7 +114,44 @@ export class CampaignCannedResponsesForm extends React.Component {
>
Add new canned response
</Button>
{this.showUploadCsvButton()}
<div>
<div className={css(styles.flexEnd)}>
<Tooltip
title="Upload a CSV of canned responses with columns for Title, Text, and Tags"
>
<IconButton
onClick={() => this.uploadCsvInputRef.current.click()}
disabled={this.state.uploadingCsv}
>
<PublishIcon />
</IconButton>
</Tooltip>
{cannedResponses.length ? (
<Tooltip title="Remove all canned responses" >
<IconButton
onClick={() => this.props.onChange({
cannedResponses: []
})}
>
<ClearIcon />
</IconButton>
</Tooltip>
) : ""}
</div>
<input
type="file"
accept=".csv"
ref={this.uploadCsvInputRef}
onChange={this.handleCsvUpload}
onClick={e => (e.target.value = null)}
style={{ display: "none" }}
/>
{this.state.uploadCsvError && (
<div className={css(styles.redText)}>
{this.state.uploadCsvError}
</div>
)}
</div>
</div>
);
}
Expand Down Expand Up @@ -275,8 +267,8 @@ export class CampaignCannedResponsesForm extends React.Component {

if (!file) return;

this.setState({ uploadingCsv: true, uploadCsvError: null }, () => {
parseCannedResponseCsv(
this.setState({ uploadingCsv: true, uploadCsvError: null },
() => parseCannedResponseCsv(
file,
tags,
({ error, cannedResponses }) => {
Expand All @@ -285,19 +277,20 @@ export class CampaignCannedResponsesForm extends React.Component {
uploadCsvError: error
});

if (!error) {
this.props.onChange({
cannedResponses: this.props.formValues.cannedResponses.concat(
cannedResponses.map(r => ({
...r,
id: this.getCannedResponseId()
}))
)
});
}
if (error) return;

this.props.onChange({
cannedResponses: [
...this.props.formValues.cannedResponses,
...cannedResponses.map(r => ({
...r,
id: this.getCannedResponseId()
}))
]
});
}
);
});
)
);
};

render() {
Expand Down Expand Up @@ -326,7 +319,7 @@ export class CampaignCannedResponsesForm extends React.Component {
subtitle="Save some scripts for your texters to use to answer additional FAQs that may come up outside of the survey questions and scripts you already set up."
/>
{list}
{this.showAddButton()}
{this.showAddButton(cannedResponses)}
<Form.Submit
as={GSSubmitButton}
disabled={this.props.saveDisabled}
Expand Down
3 changes: 2 additions & 1 deletion src/lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ export { gzip, gunzip } from "./gzip";
export {
parseCSV,
organizationCustomFields,
requiredUploadFields
requiredUploadFields,
parseCannedResponseCsv
} from "./parse_csv.js";
67 changes: 36 additions & 31 deletions src/lib/parse_csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,28 @@ export const parseCSV = (file, onCompleteCallback, options) => {
});
};

const clean = str => str.toLowerCase().trim();

const parseTags = (org_tags, tag_text) => {
console.log(tag_text);
const tagIds = [];
for (var t of tag_text.split(',')) {
const tag_name = clean(t);

if (!tag_name) continue;

const tag = org_tags.find(tag => clean(tag.name) == tag_name);

if (!tag) {
throw `"${tag_name}" cannot be found in your organization's tags`;
}

tagIds.push(tag.id);
}

return tagIds;
}

export const parseCannedResponseCsv = (file, tags, onCompleteCallback) => {
Papa.parse(file, {
header: true,
Expand All @@ -156,15 +178,14 @@ export const parseCannedResponseCsv = (file, tags, onCompleteCallback) => {
complete: ({ data: parserData, meta, errors }, file) => {
let cannedResponseRows = parserData;

const titleLabel = meta.fields.find(f => f.toLowerCase().trim() == "title");
const textLabel = meta.fields.find(f => f.toLowerCase().trim() == "text");
const tagsLabel = meta.fields.find(f => f.toLowerCase().trim() == "tags");
const titleLabel = meta.fields.find(f => clean(f) == "title");
const textLabel = meta.fields.find(f => clean(f) == "text");
const tagsLabel = meta.fields.find(f => clean(f) == "tags");

const requiredFields = [titleLabel, textLabel];
const missingFields = [];

const missingFields = requiredFields.filter(
f => meta.fields.indexOf(f) == -1
);
if (!titleLabel) missingFields.push("Title");
if (!textLabel) missingFields.push("Text");

if (missingFields.length) {
onCompleteCallback({
Expand All @@ -176,49 +197,33 @@ export const parseCannedResponseCsv = (file, tags, onCompleteCallback) => {
const cannedResponses = [];

// Loop through canned responses in CSV
for (var i in cannedResponseRows) {
const response = cannedResponseRows[i];

for (var response of cannedResponseRows) {
// Get basic details of canned response
const newCannedResponse = {
title: response[titleLabel].trim(),
text: response[textLabel].trim(),
};

// Skip line if no title/text
// Skip line if no title/text, error if only one empty
if (!newCannedResponse.title && !newCannedResponse.text) {
continue;
}

if (!newCannedResponse.title || !newCannedResponse.text) {
onCompleteCallback({
error:
`Incomplete Line. Title ${newCannedResponse.title}; Text: ${newCannedResponse.text}`
`Incomplete Line. Title: ${newCannedResponse.title}; Text: ${newCannedResponse.text}`
});
return;
}

const tagIds = [];

for (var t of response[tagsLabel].split(',')) {
const tag_name = t.trim();

if (!tag_name) continue;

const tag = tags.find(tag => tag.name.toLowerCase() == tag_name.toLowerCase());

if (!tag) {
onCompleteCallback({
error: `"${tag_name}" cannot be found in your organization's tags`
});
return;
}

tagIds.push(tag.id);
try {
newCannedResponse.tagIds = parseTags(tags, response[tagsLabel])
} catch (error) {
onCompleteCallback({ error });
return;
}

newCannedResponse.tagIds = tagIds;

cannedResponses.push(newCannedResponse);
}

Expand Down

0 comments on commit d4695dc

Please sign in to comment.