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:
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
+20
-1
@@ -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
@@ -113,6 +113,9 @@
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
[purpose='application-form-heading'] {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
[purpose='submit-button'] {
|
||||
display: flex;
|
||||
padding: 16px 32px;
|
||||
|
||||
Vendored
+1
@@ -83,4 +83,5 @@ module.exports.policies = {
|
||||
'view-okta-conditional-access-error': true,
|
||||
'view-fast-track': true,
|
||||
'vpp-proxy/*': true,
|
||||
'deliver-application-submission': true,
|
||||
};
|
||||
|
||||
Vendored
+1
@@ -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
@@ -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});
|
||||
|
||||
Vendored
+71
-7
@@ -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. We’d 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. We’d 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. We’d 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. We’d 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, we’ll 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>
|
||||
|
||||
Reference in New Issue
Block a user