Skip to content

Commit

Permalink
send project grants as multipart/form-data
Browse files Browse the repository at this point in the history
  • Loading branch information
pettinarip committed Mar 9, 2022
1 parent 2f41b4f commit 3fc24e6
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 110 deletions.
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"chakra-react-select": "^3.0.1",
"chakra-ui-markdown-renderer": "^4.0.0",
"focus-visible": "^5.2.0",
"formidable": "^2.0.1",
"framer-motion": "^4",
"google-spreadsheet": "^3.2.0",
"jsforce": "^1.11.0",
Expand All @@ -35,6 +36,7 @@
"redaxios": "^0.4.1"
},
"devDependencies": {
"@types/formidable": "^2.0.4",
"@types/google-spreadsheet": "^3.1.5",
"@types/jsforce": "^1.9.37",
"@types/mailchimp__mailchimp_marketing": "^3.0.3",
Expand Down
24 changes: 2 additions & 22 deletions src/components/forms/ProjectGrantsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import {
TOAST_OPTIONS
} from '../../constants';

import { ProjectGrantsFormData, ProposalFile, ReferralSource } from '../../types';
import { ProjectGrantsFormData, ReferralSource } from '../../types';
import { RemoveIcon } from '../UI/icons';

const MotionBox = motion<BoxProps>(Box);
Expand Down Expand Up @@ -81,26 +81,7 @@ export const ProjectGrantsForm: FC = () => {

setSelectedFile(file);

const reader = new FileReader();
// we have to encode the file content as base64 to be able to upload it to SF
reader.readAsDataURL(file);

reader.onabort = () => console.log('File reading was aborted.');
reader.onerror = () => console.error('File reading has failed.');
reader.onload = () => {
const base64 = reader.result as string;

const uploadedFile: ProposalFile = {
name: file.name,
type: file.type,
size: file.size,
// `data:*/*;base64,` needs to be removed to retrieve the base64 encoded string only
content: base64.split('base64,')[1] as string,
path: file.path
};

setValue('uploadProposal', uploadedFile, { shouldValidate: true });
};
setValue('uploadProposal', file, { shouldValidate: true });

toast({
...TOAST_OPTIONS,
Expand Down Expand Up @@ -862,7 +843,6 @@ export const ProjectGrantsForm: FC = () => {
name='uploadProposal'
control={control}
rules={{ validate: file => (file ? file.size < MAX_PROPOSAL_FILE_SIZE : true) }}
defaultValue={null}
render={({ field: { onChange } }) => (
<FormControl id='upload-proposal' {...getRootProps()}>
<InputGroup>
Expand Down
32 changes: 20 additions & 12 deletions src/components/forms/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,27 @@ export const api = {
},
projectGrants: {
submit: (data: ProjectGrantsFormData) => {
const curatedData: { [key: string]: any } = {
...data,
// Company is a required field in SF, we're using the Name as default value if no company provided
company: data.company === 'N/A' ? `${data.firstName} ${data.lastName}` : data.company,
website: getWebsite(data.website),
github: getGitHub(data.github),
projectCategory: data.projectCategory.value,
country: data.country.value,
timezone: data.timezone.value,
howDidYouHearAboutESP: data.howDidYouHearAboutESP.value
};

const formData = new FormData();

for (const name in data) {
formData.append(name, curatedData[name]);
}

const projectGrantsRequestOptions: RequestInit = {
...methodOptions,
body: JSON.stringify({
...data,
// Company is a required field in SF, we're using the Name as default value if no company provided
company: data.company === 'N/A' ? `${data.firstName} ${data.lastName}` : data.company,
website: getWebsite(data.website),
github: getGitHub(data.github),
projectCategory: data.projectCategory.value,
country: data.country.value,
timezone: data.timezone.value,
howDidYouHearAboutESP: data.howDidYouHearAboutESP.value
})
method: 'POST',
body: formData
};

return fetch(API_PROJECT_GRANTS, projectGrantsRequestOptions);
Expand Down
147 changes: 82 additions & 65 deletions src/pages/api/project-grants.ts
Original file line number Diff line number Diff line change
@@ -1,68 +1,72 @@
import fs from 'fs';
import jsforce from 'jsforce';
import { NextApiRequest, NextApiResponse } from 'next';
import formidable, { File } from 'formidable';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { body } = req;
const {
firstName: FirstName,
lastName: LastName,
email: Email,
company: Company,
projectName: Project_Name__c,
website: Website,
github: Github_Link__c,
twitter: Twitter__c,
teamProfile: Team_Profile__c,
projectDescription: Project_Description__c,
projectCategory: Category__c,
requestedAmount: Requested_Amount__c,
city: npsp__CompanyCity__c,
country: npsp__CompanyCountry__c,
timezone: Time_Zone__c,
howDidYouHearAboutESP: Referral_Source__c,
referralSourceIfOther: Referral_Source_if_Other__c,
referrals: Referrals__c,
uploadProposal
} = body;
const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } =
process.env;

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});
const form = formidable({});

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
form.parse(req, (err, fields, files) => {
if (err) {
return console.error(err);
res.status(400).json({ status: 'fail' });
return;
}

let createdLeadID: string;

// Single record creation
conn.sobject('Lead').create(
{
FirstName: FirstName.trim(),
LastName: LastName.trim(),
Email: Email.trim(),
Company: Company.trim(),
Project_Name__c: Project_Name__c.trim(),
Website: Website.trim(),
Github_Link__c: Github_Link__c.trim(),
Twitter__c: Twitter__c.trim(),
Team_Profile__c: Team_Profile__c.trim(),
Project_Description__c: Project_Description__c.trim(),
Category__c: Category__c.trim(),
Requested_Amount__c: Requested_Amount__c.trim(),
npsp__CompanyCity__c: npsp__CompanyCity__c.trim(),
npsp__CompanyCountry__c: npsp__CompanyCountry__c.trim(),
Time_Zone__c: Time_Zone__c.trim(),
Referral_Source__c: Referral_Source__c.trim(),
Referral_Source_if_Other__c: Referral_Source_if_Other__c.trim(),
Referrals__c: Referrals__c.trim(),
RecordTypeId: process.env.SF_RECORD_TYPE_PROJECT_GRANTS
},
(err, ret) => {
const fieldsSanitized = Object.keys(fields).reduce<typeof fields>((prev, key) => {
let value = fields[key];
if (typeof value === 'string') {
value = value.trim();
}

return {
...prev,
[key]: value
};
}, {});

const { SF_PROD_LOGIN_URL, SF_PROD_USERNAME, SF_PROD_PASSWORD, SF_PROD_SECURITY_TOKEN } =
process.env;

const conn = new jsforce.Connection({
// you can change loginUrl to connect to sandbox or prerelease env.
loginUrl: SF_PROD_LOGIN_URL
});

const application = {
FirstName: fieldsSanitized.firstName,
LastName: fieldsSanitized.lastName,
Email: fieldsSanitized.email,
Company: fieldsSanitized.company,
Project_Name__c: fieldsSanitized.projectName,
Website: fieldsSanitized.website,
Github_Link__c: fieldsSanitized.github,
Twitter__c: fieldsSanitized.twitter,
Team_Profile__c: fieldsSanitized.teamProfile,
Project_Description__c: fieldsSanitized.projectDescription,
Category__c: fieldsSanitized.projectCategory,
Requested_Amount__c: fieldsSanitized.requestedAmount,
npsp__CompanyCity__c: fieldsSanitized.city,
npsp__CompanyCountry__c: fieldsSanitized.country,
Time_Zone__c: fieldsSanitized.timezone,
Referral_Source__c: fieldsSanitized.howDidYouHearAboutESP,
Referral_Source_if_Other__c: fieldsSanitized.referralSourceIfOther,
Referrals__c: fieldsSanitized.referrals,
RecordTypeId: process.env.SF_RECORD_TYPE_PROJECT_GRANTS
};

res.status(200).json({ status: 'ok' });

conn.login(SF_PROD_USERNAME!, `${SF_PROD_PASSWORD}${SF_PROD_SECURITY_TOKEN}`, err => {
if (err) {
return console.error(err);
}

let createdLeadID: string;

res.status(200).json({ status: 'ok' });

// Single record creation
conn.sobject('Lead').create(application, (err, ret) => {
if (err || !ret.success) {
console.error(err);
res.status(400).json({ status: 'fail' });
Expand All @@ -72,13 +76,28 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
createdLeadID = ret.id;
console.log({ createdLeadID });

const uploadProposal = files.uploadProposal as File;
console.log({ uploadProposal });

if (uploadProposal) {
let uploadProposalContent;
try {
// turn file into base64 encoding
uploadProposalContent = fs.readFileSync(uploadProposal.filepath, {
encoding: 'base64'
});
} catch (error) {
console.error(error);
res.status(500).json({ status: 'fail' });
return;
}

// Document upload
conn.sobject('ContentVersion').create(
{
Title: `[PROPOSAL] ${Project_Name__c} - ${createdLeadID}`,
PathOnClient: uploadProposal.path,
VersionData: uploadProposal.content // base64 encoded file content
Title: `[PROPOSAL] ${application.Project_Name__c} - ${createdLeadID}`,
PathOnClient: uploadProposal.originalFilename,
VersionData: uploadProposalContent // base64 encoded file content
},
async (err, uploadedFile) => {
if (err || !uploadedFile.success) {
Expand Down Expand Up @@ -108,15 +127,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)

res.status(200).json({ status: 'ok' });
}
}
);
});
});
});
}

export const config = {
api: {
bodyParser: {
sizeLimit: '4mb'
}
bodyParser: false
}
};
10 changes: 1 addition & 9 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,6 @@ export interface GranteeFinanceAPIMap {
[preference: string]: string;
}

export type ProposalFile = {
name: string;
type: string;
size: number;
content: string;
path: string;
} | null;

export type NewsletterFormData = {
email: string;
};
Expand All @@ -52,7 +44,7 @@ export type ProjectGrantsFormData = {
howDidYouHearAboutESP: ReferralSource; // SF API: Referral_Source__c
referralSourceIfOther: string; // SF API: Referral_Source_if_Other__c
referrals: string; // SF API: Referrals__c
uploadProposal: ProposalFile;
uploadProposal: File;
};

export type SmallGrantsFormData = {
Expand Down
39 changes: 37 additions & 2 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,13 @@
dependencies:
"@types/ms" "*"

"@types/formidable@^2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@types/formidable/-/formidable-2.0.4.tgz#fdda9c4952ac7c7866485fea8e15e2cbcebfcc44"
integrity sha512-6HYcnmBCeby/nNGgX9kq1DxUpK2UcB3yoHCr3GzFjjqkpivOdcBSbsXP9NbxLcPEi11Fl/L41rbFCIsteF9sbg==
dependencies:
"@types/node" "*"

"@types/google-spreadsheet@^3.1.5":
version "3.1.5"
resolved "https://registry.yarnpkg.com/@types/google-spreadsheet/-/google-spreadsheet-3.1.5.tgz#2bdc6f9f5372551e0506cb6ef3f562adcf44fc2e"
Expand Down Expand Up @@ -1313,7 +1320,7 @@ arrify@^2.0.0:
resolved "https://registry.yarnpkg.com/arrify/-/arrify-2.0.1.tgz#c9655e9331e0abcd588d2a7cad7e9956f66701fa"
integrity sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==

asap@*, asap@~2.0.3:
asap@*, asap@^2.0.0, asap@~2.0.3:
version "2.0.6"
resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
Expand Down Expand Up @@ -2005,6 +2012,14 @@ detect-node-es@^1.1.0:
resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493"
integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==

dezalgo@1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/dezalgo/-/dezalgo-1.0.3.tgz#7f742de066fc748bc8db820569dddce49bf0d456"
integrity sha1-f3Qt4Gb8dIvI24IFad3c5Jvw1FY=
dependencies:
asap "^2.0.0"
wrappy "1"

diff@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
Expand Down Expand Up @@ -2597,6 +2612,16 @@ formidable@^1.1.1:
resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.2.6.tgz#d2a51d60162bbc9b4a055d8457a7c75315d1a168"
integrity sha512-KcpbcpuLNOwrEjnbpMC0gS+X8ciDoZE1kkqzat4a8vrprf+s9pKNQ/QIwWfbfs4ltgmFl3MD177SNTkve3BwGQ==

formidable@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/formidable/-/formidable-2.0.1.tgz#4310bc7965d185536f9565184dee74fbb75557ff"
integrity sha512-rjTMNbp2BpfQShhFbR3Ruk3qk2y9jKpvMW78nJgx8QKtxjDVrwbZG+wvDOmVbifHyOUOQJXxqEy6r0faRrPzTQ==
dependencies:
dezalgo "1.0.3"
hexoid "1.0.0"
once "1.4.0"
qs "6.9.3"

framer-motion@^4:
version "4.1.17"
resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-4.1.17.tgz#4029469252a62ea599902e5a92b537120cc89721"
Expand Down Expand Up @@ -2873,6 +2898,11 @@ he@1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==

hexoid@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/hexoid/-/hexoid-1.0.0.tgz#ad10c6573fb907de23d9ec63a711267d9dc9bc18"
integrity sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==

hey-listen@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68"
Expand Down Expand Up @@ -3968,7 +3998,7 @@ object.values@^1.1.5:
define-properties "^1.1.3"
es-abstract "^1.19.1"

once@^1.3.0:
once@1.4.0, once@^1.3.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E=
Expand Down Expand Up @@ -4253,6 +4283,11 @@ punycode@^2.1.0, punycode@^2.1.1:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==

qs@6.9.3:
version "6.9.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.3.tgz#bfadcd296c2d549f1dffa560619132c977f5008e"
integrity sha512-EbZYNarm6138UKKq46tdx08Yo/q9ZhFoAXAI1meAFd2GtbRDhbZY2WQSICskT0c5q99aFzLG1D4nvTk9tqfXIw==

qs@^6.5.1:
version "6.10.3"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.10.3.tgz#d6cde1b2ffca87b5aa57889816c5f81535e22e8e"
Expand Down

0 comments on commit 3fc24e6

Please sign in to comment.