Website: update contact form for applications (#38125)

Changes:
- Updated the contact form to have a third form for job applications.
The form is only shown to users who visit the page with the `#apply`
hash.
- Added `deliver-application-submission` an action that sends
information from job application submissions to a zapier webhook.
- Updated the link to the contact form on the open positions template
page to link to /contact#apply
This commit is contained in:
Eric
2026-01-12 11:01:02 -06:00
committed by GitHub
parent 212e1ee338
commit 5219adb7b3
9 changed files with 211 additions and 12 deletions
@@ -0,0 +1,98 @@
module.exports = {
friendlyName: 'Deliver application submission',
description: 'Delivers form submissions from the application form on the contact page to a Zapier webhook.',
inputs: {
firstName: {
required: true,
type: 'string',
description: 'The first name of the applicant.',
},
lastName: {
required: true,
type: 'string',
description: 'The last name of the applicant.',
},
emailAddress: {
required: true,
isEmail: true,
type: 'string',
description: 'A return email address where we can respond.',
},
position: {
type: 'string',
required: true,
description: 'The open position this applicant is applying for.'
},
linkedinProfileUrl: {
type: 'string',
required: true,
description: 'The URL of the applicant\'s LinkedIn profile.'
},
location: {
type: 'string',
required: true,
description: 'The location of the applicant'
},
message: {
type: 'string',
required: true,
description: 'The applican\'ts cover letter in plain text.',
},
},
exits: {
success: {
description: 'A job application was successfully submitted.',
}
},
fn: async function ({ firstName, lastName, emailAddress, position, linkedinProfileUrl, location, message,}) {
// Send the submitted information to a Zapier webhook.
await sails.helpers.http.post.with({
url: 'https://hooks.zapier.com/hooks/catch/3627242/uwc77dr/',
data: {
firstName,
lastName,
emailAddress,
position: position.replace(/🧑‍🚀|🚀|🌦️|🎐|🫧|🐋|🦢|🌐/, ''),// Remove the emoji from the job title.
linkedinProfileUrl,
location,
message,
webhookSecret: sails.config.custom.zapierWebhookSecret
}
})
.timeout(5000)
.tolerate(['non200Response', 'requestFailed', {name: 'TimeoutError'}], (err)=>{
// Note that Zapier responds with a 2xx status code even if something goes wrong, so just because this message is not logged doesn't mean everything is hunky dory. More info: https://github.com/fleetdm/fleet/pull/6380#issuecomment-1204395762
sails.log.warn(`When a user submitted the application form, an error occurred while sending a request to Zapier. Raw error: ${require('util').inspect(err)}`);
return;
});
// All done.
return;
}
};
+15 -2
View File
@@ -18,13 +18,21 @@ module.exports = {
success: {
viewTemplatePath: 'pages/contact'
}
},
badConfig: {
responseType: 'badConfig'
},
},
fn: async function ({sendMessage}) {
if (!_.isObject(sails.config.builtStaticContent) || !_.isArray(sails.config.builtStaticContent.openPositions)) {
throw {badConfig: 'builtStaticContent.openPositions'};
}
let formToShow = 'talk-to-us';
let userIsLoggedIn = !! this.req.me;
@@ -40,11 +48,16 @@ module.exports = {
if(sendMessage) {
formToShow = 'contact';
}
let currentOpenPositionsForApplicationDropdown = _.pluck(sails.config.builtStaticContent.openPositions, 'jobTitle');
// Respond with view.
return {
formToShow,
userIsLoggedIn,
userHasPremiumSubscription
userHasPremiumSubscription,
currentOpenPositionsForApplicationDropdown
};
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+20 -1
View File
@@ -29,8 +29,19 @@ parasails.registerPage('contact', {
emailAddress: {isEmail: true, required: true},
firstName: {required: true},
lastName: {required: true},
message: {required: false},
message: {required: true},
},
// Application form rules
applicationFormRules: {
emailAddress: {isEmail: true, required: true},
firstName: {required: true},
lastName: {required: true},
linkedinProfileUrl: {required: true},
position: {required: true},
location: {required: true},
message: {required: true},
},
formDataToPrefillForLoggedInUsers: {},
// Server error state for the form
@@ -74,6 +85,9 @@ parasails.registerPage('contact', {
if (window.location.hash === '#message') {// prefill from URL bar
this.formToDisplay = 'contact';
}
if (window.location.hash === '#apply') {// prefill from URL bar
this.formToDisplay = 'apply';
}
},
mounted: async function() {
//…
@@ -114,6 +128,11 @@ parasails.registerPage('contact', {
this.goto(report.eventUrl);
},
submittedApplicationForm: async function() {
// Show the success message.
this.cloudSuccess = true;
},
clickSwitchForms: function(form) {
if(this.me){
this.formData = _.clone(this.formDataToPrefillForLoggedInUsers);
+3
View File
@@ -113,6 +113,9 @@
display: block;
}
}
[purpose='application-form-heading'] {
margin-bottom: 32px;
}
[purpose='submit-button'] {
display: flex;
padding: 16px 32px;
+1
View File
@@ -83,4 +83,5 @@ module.exports.policies = {
'view-okta-conditional-access-error': true,
'view-fast-track': true,
'vpp-proxy/*': true,
'deliver-application-submission': true,
};
+1
View File
@@ -1260,6 +1260,7 @@ module.exports.routes = {
'POST /api/v1/customers/get-stripe-checkout-session-url': { action: 'customers/get-stripe-checkout-session-url' },
'/api/v1/query-generator/get-llm-generated-sql': { action: 'query-generator/get-llm-generated-sql' },
'POST /api/v1/get-llm-generated-configuration-profile': { action: 'get-llm-generated-configuration-profile', hasSocketFeatures: true },
'POST /api/v1/deliver-application-submission': { action: 'deliver-application-submission' },
+1 -1
View File
@@ -859,7 +859,7 @@ module.exports = {
}
let pageTitle = openPosition.jobTitle;
let mdStringForThisOpenPosition = `# ${openPosition.jobTitle}\n\n## Let's start with why we exist. 📡\n\nEver wondered if your employer is monitoring your work computer?\n\nOrganizations make huge investments every year to keep their laptops and servers online, secure, compliant, and usable from anywhere. This is called "device management".\n\nAt Fleet, we think it's time device management became [transparent](https://fleetdm.com/transparency) and [open source](https://fleetdm.com/handbook/company#open-source).\n\n\n## About the company 🌈\n\nYou can read more about the company in our [handbook](https://fleetdm.com/handbook/company), which is public and open to the world.\n\ntldr; Fleet Device Management Inc. is a [Series B](https://www.businesswire.com/news/home/20250617550974/en/Fleet-Adds-%2427M-to-Usher-in-New-Era-of-Open-Device-Management) startup founded and backed by the same people who created osquery, the leading open source security agent. Today, osquery is installed on millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community).\n\n\n## Your primary responsibilities 🔭\n${openPosition.responsibilities}\n\n## Are you our new team member? 🧑‍🚀\nIf most of these qualities sound like you, we would love to chat and see if we're a good fit.\n\n${openPosition.experience}\n\n## Why should you join us? 🛸\n\nLearn more about the company and [why you should join us here](https://fleetdm.com/handbook/company#is-it-any-good).\n\n<div purpose="open-position-quote-card"><div><img alt="Deloitte logo" src="/images/logo-deloitte-166x36@2x.png"></div><div purpose="open-position-quote"><div purpose="quote-text"><p>“One of the best teams out there to go work for and help shape security platforms.”</p></div></div></div>\n\n\n## Want to join the team?\n\nWant to join the team?\n\n[Message us your Linkedin profile](/contact#message). \n\n\n >The salary range for this role is ${openPosition.onTargetEarnings ? openPosition.onTargetEarnings : '$48,000 - $480,000'}. Fleet provides competitive compensation based on our [compensation philosophy](https://fleetdm.com/handbook/company/communications#compensation), as well as comprehensive [benefits](https://fleetdm.com/handbook/company/communications#benefits).`;
let mdStringForThisOpenPosition = `# ${openPosition.jobTitle}\n\n## Let's start with why we exist. 📡\n\nEver wondered if your employer is monitoring your work computer?\n\nOrganizations make huge investments every year to keep their laptops and servers online, secure, compliant, and usable from anywhere. This is called "device management".\n\nAt Fleet, we think it's time device management became [transparent](https://fleetdm.com/transparency) and [open source](https://fleetdm.com/handbook/company#open-source).\n\n\n## About the company 🌈\n\nYou can read more about the company in our [handbook](https://fleetdm.com/handbook/company), which is public and open to the world.\n\ntldr; Fleet Device Management Inc. is a [Series B](https://www.businesswire.com/news/home/20250617550974/en/Fleet-Adds-%2427M-to-Usher-in-New-Era-of-Open-Device-Management) startup founded and backed by the same people who created osquery, the leading open source security agent. Today, osquery is installed on millions of laptops and servers, and it is especially popular with [enterprise IT and security teams](https://www.linuxfoundation.org/press/press-release/the-linux-foundation-announces-intent-to-form-new-foundation-to-support-osquery-community).\n\n\n## Your primary responsibilities 🔭\n${openPosition.responsibilities}\n\n## Are you our new team member? 🧑‍🚀\nIf most of these qualities sound like you, we would love to chat and see if we're a good fit.\n\n${openPosition.experience}\n\n## Why should you join us? 🛸\n\nLearn more about the company and [why you should join us here](https://fleetdm.com/handbook/company#is-it-any-good).\n\n<div purpose="open-position-quote-card"><div><img alt="Deloitte logo" src="/images/logo-deloitte-166x36@2x.png"></div><div purpose="open-position-quote"><div purpose="quote-text"><p>“One of the best teams out there to go work for and help shape security platforms.”</p></div></div></div>\n\n\n## Want to join the team?\n\nWant to join the team?\n\n[Message us your Linkedin profile](/contact#apply). \n\n\n >The salary range for this role is ${openPosition.onTargetEarnings ? openPosition.onTargetEarnings : '$48,000 - $480,000'}. Fleet provides competitive compensation based on our [compensation philosophy](https://fleetdm.com/handbook/company/communications#compensation), as well as comprehensive [benefits](https://fleetdm.com/handbook/company/communications#benefits).`;
let htmlStringForThisPosition = await sails.helpers.strings.toHtml.with({mdString: mdStringForThisOpenPosition});
+71 -7
View File
@@ -2,12 +2,17 @@
<div purpose="page-container" class="container-fluid">
<div class="d-flex flex-lg-row flex-column justify-content-center">
<div purpose="form-container" v-if="!cloudSuccess">
<h2>Get in touch</h2>
<p v-if="userHasPremiumSubscription" style="margin-bottom: 40px;">Dedicated professional support from the Fleet team.</p>
<p v-else-if="psychologicalStage === '4 - Has use case'">Let us help you deploy and evaluate Fleet quickly for yourself. Wed love to save you some time.</p>
<p v-else-if="psychologicalStage === '5 - Personally confident'">Schedule a personalized demo for your team and get support or training.</p>
<p v-else>Schedule a personalized demo, or ask us anything. Wed love to chat.</p>
<div purpose="contact-form-switch" class="d-flex flex-sm-row flex-column justify-content-center mx-auto" v-if="!userHasPremiumSubscription">
<div v-if="formToDisplay !== 'apply'">
<h2>Get in touch</h2>
<p v-if="userHasPremiumSubscription" style="margin-bottom: 40px;">Dedicated professional support from the Fleet team.</p>
<p v-else-if="psychologicalStage === '4 - Has use case'">Let us help you deploy and evaluate Fleet quickly for yourself. Wed love to save you some time.</p>
<p v-else-if="psychologicalStage === '5 - Personally confident'">Schedule a personalized demo for your team and get support or training.</p>
<p v-else>Schedule a personalized demo, or ask us anything. Wed love to chat.</p>
</div>
<div v-else>
<h2 purpose="application-form-heading">Apply for an open position</h2>
</div>
<div purpose="contact-form-switch" class="d-flex flex-sm-row flex-column justify-content-center mx-auto" v-if="!userHasPremiumSubscription && formToDisplay !== 'apply'">
<div purpose="switch-option" :class="[formToDisplay === 'talk-to-us' ? 'selected' : '']" @click="clickSwitchForms('talk-to-us')">Get a demo</div>
<div purpose="switch-option" :class="[formToDisplay === 'contact' ? 'selected' : '']" @click="clickSwitchForms('contact')">Send a message</div>
<div purpose="switch" :class="formToDisplay+'-selected'"></div>
@@ -65,7 +70,7 @@
</div>
</ajax-form>
</div>
<div v-else>
<div v-else-if="formToDisplay === 'talk-to-us'">
<ajax-form :handle-submitting="handleSubmittingTalkToUsForm" class="contact" :form-errors.sync="formErrors" :form-data="formData" :form-rules="talkToUsFormRules" :cloud-error.sync="cloudError">
<div class="form-group">
<label for="email-address">Work email *</label>
@@ -132,6 +137,65 @@
</div>
</ajax-form>
</div>
<div v-else-if="formToDisplay === 'apply'">
<ajax-form action="deliverApplicationSubmission" class="apply" :form-errors.sync="formErrors" :form-data="formData" :form-rules="applicationFormRules" :syncing.sync="syncing" :cloud-error.sync="cloudError" @submitted="submittedApplicationForm()">
<div class="form-group">
<div class="row">
<div purpose="first-name-column" class="col-sm mb-4 mb-sm-0">
<label for="apply-first-name">First name</label>
<input class="form-control" id="apply-first-name" name="first-name" type="text" :class="[formErrors.firstName ? 'is-invalid' : '']" v-model.trim="formData.firstName" autocomplete="given-name" focus-first>
<div class="invalid-feedback" v-if="formErrors.firstName">Please let us know what to call you.</div>
</div>
<div purpose="last-name-column" class="col-sm">
<label for="apply-last-name">Last name</label>
<input class="form-control" id="apply-last-name" name="last-name" type="text" :class="[formErrors.lastName ? 'is-invalid' : '']" v-model.trim="formData.lastName" autocomplete="family-name">
<div class="invalid-feedback" v-if="formErrors.lastName">Please let us know what to call you.</div>
</div>
</div>
</div>
<div class="form-group">
<label for="apply-email-address">Email</label>
<input class="form-control" id="apply-email-address" name="email-address" type="email" :class="[formErrors.emailAddress ? 'is-invalid' : cloudError && cloudError === 'invalidEmailDomain' ? 'is-invalid' : '']" v-model.trim="formData.emailAddress" autocomplete="email" >
<div class="invalid-feedback" v-if="formErrors.emailAddress">Please enter a valid email address</div>
</div>
<div class="form-group">
<label for="apply-position">Position</label>
<select class="form-control custom-select" v-model.trim="formData.position" :class="[formErrors.position ? 'is-invalid' : '']">
<option :value="undefined">Select an option</option>
<option :value="position" v-for="position in currentOpenPositionsForApplicationDropdown">{{position}}</option>
</select>
<div class="invalid-feedback">Please select an open position.</div>
</div>
<div class="form-group">
<label for="apply-linkedin-profile">LinkedIn profile</label>
<input class="form-control" id="apply-linkedin-profile" name="LinkedIn profile" type="text" :class="[formErrors.linkedinProfileUrl ? 'is-invalid' : '']" v-model.trim="formData.linkedinProfileUrl" autocomplete="email">
<div class="invalid-feedback" v-if="formErrors.linkedinProfileUrl">Please provide a link to your LinkedIn profile.</div>
</div>
<div class="form-group">
<label for="apply-location">Location</label>
<input class="form-control" id="apply-location" name="location" type="text" :class="[formErrors.location ? 'is-invalid' : '']" v-model.trim="formData.location" >
<div class="invalid-feedback" v-if="formErrors.location">Please let us know where you are located.</div>
</div>
<div class="form-group">
<label for="apply-message">Cover letter</label>
<textarea class="form-control" id="apply-message" name="message" :class="[formErrors.message ? 'is-invalid' : '']" v-model.trim="formData.message" autocomplete="none"></textarea>
<div class="invalid-feedback" v-if="formErrors.message">Cover letter cannot be empty.</div>
</div>
<cloud-error v-if="cloudError"></cloud-error>
<div class="form-group btn-container">
<ajax-button purpose="submit-button" type="submit" :syncing="syncing" class="btn btn-primary">Send</ajax-button>
</div>
</ajax-form>
</div>
</div>
<div purpose="success-message" v-else-if="cloudSuccess && formToDisplay === 'apply'">
<h2>Thanks for applying</h2>
<p class="mt-3">If your background is a good fit, well be in touch within about a week. If not, please feel free to apply again in the future.</p>
</div>
<div purpose="success-message" v-else>
<h2>Thank you!</h2>