-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add support for sending ad hoc batch emails #234
Changes from 3 commits
58afd75
92f6d44
51189c9
049d355
3b06a53
8f8d0f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,7 @@ <h1 class="center">Admin Panel</h1> | |
<a href="#users" class="btn">Users</a> | ||
<a href="#applicants" class="btn">Applicants</a> | ||
<a href="#settings" class="btn">Settings</a> | ||
<a href="#emails" class="btn">Emails</a> | ||
</nav> | ||
|
||
<section id="statistics"> | ||
|
@@ -412,6 +413,42 @@ <h4><code>config.json</code> options</h4> | |
</div> | ||
</form> | ||
</section> | ||
<section id="emails"> | ||
<h2>Batch Emails</h2> | ||
<select id="email-branch-filter"> | ||
<option value="*">All accounts</option> | ||
<option value="na">Not Applied</option> | ||
<optgroup label="Applicant Type"> | ||
{{#each settings.branches.application}} | ||
<option value="application-{{this.name}}">{{this.name}}</option> | ||
{{/each}} | ||
{{#each settings.branches.confirmation}} | ||
<option value="confirmation-{{this.name}}">{{this.name}}</option> | ||
{{/each}} | ||
</optgroup> | ||
</select> | ||
<select id="email-status-filter"> | ||
</select> | ||
|
||
<input type="text" id="batch-email-subject" placeholder="Email Subject"/> | ||
<textarea id="batch-email-content"></textarea> | ||
<h5>Rendered HTML and text:</h5> | ||
<section id="batch-email-rendered"></section> | ||
|
||
<h5>List of variables:</h5> | ||
<ul> | ||
<li><strong>\{{eventName}}</strong>: The name of the event, configured by the <code>eventName</code> key-value pair in the <code>config.json</code> and displayed at the top of the page.</li> | ||
<li><strong>\{{email}}</strong>: The user's email as reported by them or a 3rd party OAuth provider (i.e. Google, GitHub, Facebook).</li> | ||
<li><strong>\{{name}}</strong>: The user's name as reported by them or a 3rd party OAuth provider (i.e. Google, GitHub, Facebook).</li> | ||
<li><strong>\{{teamName}}</strong>: The user's team name if teams are enabled and the user has joined a team. Otherwise, will output <code>Teams not enabled</code> or <code>No team created or joined</code> respectively.</li> | ||
<li><strong>\{{applicationBranch}}</strong>: The question branch name that the user applied / was accepted to.</li> | ||
<li><strong>\{{confirmationBranch}}</strong>: The question branch name that the user RSVPed to.</li> | ||
<li><strong>\{{application.<code>question-name</code>}}</strong>: Prints the user's response to the application question with the specified name from <code>questions.json</code>. Note that the question name is different from the question label. <a href="https://github.com/HackGT/registration/blob/master/server/config/questions.json" target="_blank">See the GitHub project</a> for details. Will print <code>N/A</code> if not yet answered.</li> | ||
<li><strong>\{{confirmation.<code>question-name</code>}}</strong>: Prints the user's response to the confirmation question with the specified name from <code>questions.json</code>.</li> | ||
<li><strong>\{{reimbursementAmount}}</strong>: A string representing how much a user should be reimbursed for.</li> | ||
</ul> | ||
<button id="sendEmail">Send Email</button> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should be centered by wrapping in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done |
||
</section> | ||
{{/sidebar}} | ||
</body> | ||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -25,7 +25,61 @@ class State { | |
this.sectionElement.style.display = "block"; | ||
} | ||
} | ||
const states: State[] = ["statistics", "users", "applicants", "settings"].map(id => new State(id)); | ||
const states: State[] = ["statistics", "users", "applicants", "settings", "emails"].map(id => new State(id)); | ||
|
||
function generateFilter(branchFilter: HTMLInputElement, statusFilter: HTMLInputElement) { | ||
let filter: any = {}; | ||
if (branchFilter.value !== "*" && branchFilter.value !== "na") { | ||
let [, type, branchName] = branchFilter.value.match(/^(application|confirmation)-(.*)$/)!; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good use of destructuring 👍 |
||
if (type === "application") { | ||
filter.applicationBranch = branchName; | ||
} | ||
else if (type === "confirmation") { | ||
filter.confirmationBranch = branchName; | ||
} | ||
switch (statusFilter.value) { | ||
case "no-submission": | ||
if (type === "confirmation") { | ||
filter.confirmed = false; | ||
} | ||
break; | ||
case "submitted": | ||
if (type === "confirmation") { | ||
filter.confirmed = true; | ||
} else { | ||
filter.applied = true; | ||
} | ||
break; | ||
} | ||
} else if (branchFilter.value === "na") { | ||
filter.applied = false; | ||
} | ||
return filter; | ||
} | ||
const batchEmailBranchFilterSelect = document.getElementById("email-branch-filter") as HTMLSelectElement; | ||
const batchEmailStatusFilterSelect = document.getElementById("email-status-filter") as HTMLSelectElement; | ||
async function batchEmailTypeChange(): Promise<void> { | ||
if (batchEmailBranchFilterSelect.value === "*" || batchEmailBranchFilterSelect.value === "na") { | ||
batchEmailStatusFilterSelect.style.display = "none"; | ||
} else { | ||
for (let i = 0; i < batchEmailBranchFilterSelect.options.length; i++) { | ||
batchEmailStatusFilterSelect.options.remove(0); | ||
} | ||
batchEmailStatusFilterSelect.style.display = "block"; | ||
let [, type ] = batchEmailBranchFilterSelect.value.match(/^(application|confirmation)-(.*)$/)!; | ||
// Only confirmation branches have no-submission option since confirmation is manually assigned | ||
if (type === "confirmation") { | ||
let noSubmission = new Option("Have not submitted (Confirmation)", "no-submission"); | ||
batchEmailStatusFilterSelect.add(noSubmission); | ||
} | ||
let submitted = new Option(`Submitted (${type.charAt(0).toUpperCase() + type.slice(1)})`, "submitted"); | ||
batchEmailStatusFilterSelect.add(submitted); | ||
} | ||
} | ||
batchEmailBranchFilterSelect.addEventListener("change", batchEmailTypeChange); | ||
batchEmailTypeChange().catch(err => { | ||
console.error(err); | ||
}); | ||
|
||
class UserEntries { | ||
private static readonly NODE_COUNT = 20; | ||
|
@@ -833,3 +887,60 @@ for (let i = 0; i < data.length; i++) { | |
} | ||
}); | ||
} | ||
|
||
let emailBranchFilter = document.getElementById("email-branch-filter") as HTMLInputElement; | ||
let emailStatusFilter = document.getElementById("email-status-filter") as HTMLInputElement; | ||
let sendEmailButton = document.getElementById("sendEmail") as HTMLButtonElement; | ||
let batchEmailSubject = document.getElementById("batch-email-subject") as HTMLInputElement; | ||
let batchEmailEditor = new SimpleMDE({ element: document.getElementById("batch-email-content")! }); | ||
let batchEmailRenderedArea: HTMLElement | ShadowRoot = document.getElementById("batch-email-rendered") as HTMLElement; | ||
if (document.head.attachShadow) { | ||
// Browser supports Shadow DOM | ||
batchEmailRenderedArea = batchEmailRenderedArea.attachShadow({ mode: "open" }); | ||
} | ||
batchEmailEditor.codemirror.on("change", async () => { | ||
try { | ||
let content = new FormData(); | ||
content.append("content", batchEmailEditor.value()); | ||
let { html, text }: { html: string; text: string } = ( | ||
await fetch(`/api/settings/email_content/batch_email/rendered`, { | ||
credentials: "same-origin", | ||
method: "POST", | ||
body: content | ||
}).then(checkStatus).then(parseJSON) | ||
); | ||
batchEmailRenderedArea.innerHTML = html; | ||
let hr = document.createElement("hr"); | ||
hr.style.border = "1px solid #737373"; | ||
batchEmailRenderedArea.appendChild(hr); | ||
let textContainer = document.createElement("pre"); | ||
textContainer.textContent = text; | ||
batchEmailRenderedArea.appendChild(textContainer); | ||
} | ||
catch { | ||
batchEmailRenderedArea.textContent = "Couldn't retrieve email content"; | ||
} | ||
}); | ||
sendEmailButton.addEventListener("click", () => { | ||
let subject = batchEmailSubject.value; | ||
let markdownContent = batchEmailEditor.value(); | ||
if (subject === "") { | ||
return sweetAlert("Oh no!", "You need an email subject", "error"); | ||
} else if (markdownContent === "") { | ||
return sweetAlert("Oh no!", "Your email body is empty.", "error"); | ||
} | ||
let filter = generateFilter(emailBranchFilter, emailStatusFilter); | ||
let content = new FormData(); | ||
content.append("filter", JSON.stringify(filter)); | ||
content.append("subject", subject); | ||
content.append("markdownContent", markdownContent); | ||
sendEmailButton.disabled = true; | ||
return fetch(`/api/settings/send_batch_email`, { | ||
credentials: "same-origin", | ||
method: "POST", | ||
body: content | ||
}).then(checkStatus).then(parseJSON).then((result: {success: boolean; count: number} ) => { | ||
sendEmailButton.disabled = false; | ||
sweetAlert("Success!", `Successfully sent ${result.count} email(s)!`, "success"); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please move this to after Applicants and before Settings
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done