Compare commits
33
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acd41b6081 | ||
|
|
62a9d8372b | ||
|
|
17a111f324 | ||
|
|
4f43446237 | ||
|
|
18188ae623 | ||
|
|
b9dd93ab87 | ||
|
|
f7088b29cc | ||
|
|
90a6a9d058 | ||
|
|
e0b60eb3b6 | ||
|
|
f598b6886f | ||
|
|
54bd3c38d5 | ||
|
|
55317c8fad | ||
|
|
3d841369da | ||
|
|
f9900947dc | ||
|
|
11886a4e5e | ||
|
|
ea94a1aff6 | ||
|
|
4e7c65f11c | ||
|
|
77364eaa07 | ||
|
|
74f046be75 | ||
|
|
0d9a94e7f5 | ||
|
|
6621130669 | ||
|
|
d7baaaf83b | ||
|
|
08875ebe61 | ||
|
|
55e1cd92ac | ||
|
|
f777fbe0a2 | ||
|
|
922db887e8 | ||
|
|
2d69cd0fdb | ||
|
|
89d013882f | ||
|
|
0073876c4b | ||
|
|
4d149e7091 | ||
|
|
a649c2733f | ||
|
|
17cb938bc8 | ||
|
|
551a6b3c02 |
@@ -49,6 +49,9 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
|
||||
Rails.logger.info "[Agent#bulk_create] ignoring email #{email}, errors: #{e.record.errors}"
|
||||
end
|
||||
end
|
||||
# remove onboarding_step key in present in Current account custom attributes
|
||||
Current.account.custom_attributes.delete('onboarding_step')
|
||||
Current.account.save!
|
||||
head :ok
|
||||
end
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ class Api::V1::AccountsController < Api::BaseController
|
||||
def update
|
||||
@account.assign_attributes(account_params.slice(:name, :locale, :domain, :support_email, :auto_resolve_duration))
|
||||
@account.custom_attributes.merge!(custom_attributes_params)
|
||||
@account.custom_attributes['onboarding_step'] = 'invite_team' if @account.custom_attributes['onboarding_step'] == 'account_update'
|
||||
@account.save!
|
||||
end
|
||||
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
/* global axios */
|
||||
|
||||
import ApiClient from './ApiClient';
|
||||
|
||||
class Agents extends ApiClient {
|
||||
constructor() {
|
||||
super('agents', { accountScoped: true });
|
||||
}
|
||||
|
||||
bulkInvite({ emails }) {
|
||||
return axios.post(`${this.url}/bulk_create`, {
|
||||
emails,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default new Agents();
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<banner
|
||||
v-if="shouldShowBanner"
|
||||
v-if="false"
|
||||
color-scheme="alert"
|
||||
:banner-message="bannerMessage"
|
||||
:action-button-label="actionButtonMessage"
|
||||
@@ -34,6 +34,9 @@ export default {
|
||||
actionButtonMessage() {
|
||||
return this.$t('GENERAL_SETTINGS.OPEN_BILLING');
|
||||
},
|
||||
isOnboardingView() {
|
||||
return this.$route.name.includes('onboarding_');
|
||||
},
|
||||
shouldShowBanner() {
|
||||
if (!this.isOnChatwootCloud) {
|
||||
return false;
|
||||
@@ -43,6 +46,9 @@ export default {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.isOnboardingView) {
|
||||
return false;
|
||||
}
|
||||
return this.isPaymentPending();
|
||||
},
|
||||
},
|
||||
|
||||
@@ -29,7 +29,13 @@ export default {
|
||||
actionButtonMessage() {
|
||||
return this.$t('APP_GLOBAL.RESEND_VERIFICATION_MAIL');
|
||||
},
|
||||
isOnboardingView() {
|
||||
return this.$route.name.includes('onboarding_');
|
||||
},
|
||||
shouldShowBanner() {
|
||||
if (this.isOnboardingView) {
|
||||
return false;
|
||||
}
|
||||
return !this.currentUser.confirmed;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -61,3 +61,7 @@ export default {
|
||||
},
|
||||
};
|
||||
export const DEFAULT_REDIRECT_URL = '/app/';
|
||||
|
||||
export const ONBOARDING_STEP_NAMES = {
|
||||
INVITE: 'invite_team',
|
||||
};
|
||||
|
||||
@@ -40,5 +40,88 @@
|
||||
},
|
||||
"SUBMIT": "Create account",
|
||||
"HAVE_AN_ACCOUNT": "Already have an account?"
|
||||
},
|
||||
"START_ONBOARDING": {
|
||||
"PROFILE": {
|
||||
"INTRO": "Welcome, let’s get started.",
|
||||
"TITLE": "Set up profile",
|
||||
"BODY": "These are details visible to end users, to make a positive first impression.",
|
||||
"SUBMIT": {
|
||||
"BUTTON": "Continue to company setup",
|
||||
"ERROR": "Error in setting up profile, try again.",
|
||||
"SUCCESS": "🔥 Lets add your business details"
|
||||
},
|
||||
"AVATAR": {
|
||||
"LABEL": "Avatar",
|
||||
"PLACEHOLDER": "Upload Profile Image"
|
||||
},
|
||||
"FULL_NAME": {
|
||||
"LABEL": "Full name",
|
||||
"PLACEHOLDER": "Enter your full name",
|
||||
"ERROR": "Add a valid full name"
|
||||
},
|
||||
"DISPLAY_NAME": {
|
||||
"LABEL": "Display name (Optional)",
|
||||
"PLACEHOLDER": "Enter the name you want to display your customers",
|
||||
"ERROR": "Add a valid display name or leave it blank"
|
||||
},
|
||||
"PHONE_NUMBER": {
|
||||
"LABEL": "Phone number (Optional)",
|
||||
"PLACEHOLDER": "Enter your phone number"
|
||||
},
|
||||
"SIGNATURE": {
|
||||
"LABEL": "Signature",
|
||||
"PLACEHOLDER": "Add a signature for your messages. You can set this later in your settings."
|
||||
}
|
||||
},
|
||||
"COMPANY": {
|
||||
"PAGE_TITLE": "Hello %{userName},\nwe are are almost done.",
|
||||
"TITLE": "Tell us about your company",
|
||||
"BODY": "In this section, we'll set up the language, timezone, and company size. This information allows us to provide contextually accurate information and suggest relevant features for you.",
|
||||
"SUBMIT": {
|
||||
"BUTTON": "Continue to team setup",
|
||||
"ERROR": "Error in setting up company, try again.",
|
||||
"SUCCESS": "🚀 Lets invite your team"
|
||||
},
|
||||
"COMPANY_NAME": {
|
||||
"LABEL": "Company name",
|
||||
"PLACEHOLDER": "Enter your company name",
|
||||
"ERROR": "Company name is too short."
|
||||
},
|
||||
"INDUSTRY": {
|
||||
"LABEL": "Industry",
|
||||
"PLACEHOLDER": "Select your industry"
|
||||
},
|
||||
"TIMEZONE": {
|
||||
"LABEL": "Timezone",
|
||||
"PLACEHOLDER": "What timezone do you work from?"
|
||||
},
|
||||
"LOCALE": {
|
||||
"LABEL": "Language",
|
||||
"PLACEHOLDER": "Select the language you want to use"
|
||||
},
|
||||
"SIZE": {
|
||||
"LABEL": "Company size",
|
||||
"PLACEHOLDER": "How many people work at your company?"
|
||||
}
|
||||
},
|
||||
"INVITE_TEAM": {
|
||||
"TITLE": "Invite your team",
|
||||
"ADD": "Add",
|
||||
"LIST_EMPTY": "No members to invite",
|
||||
"BODY": "Invite your team members to Chatwoot. You can add them later as well.",
|
||||
"PLACEHOLDER": "robin@chatwoot.com",
|
||||
"SKIP": "Skip for now",
|
||||
"SUBMIT": {
|
||||
"BUTTON": "Send invitations",
|
||||
"ERROR": "Error while inviting team members, try again.",
|
||||
"NO_MEMBER_ERROR": "Please add atleast one team member or skip this step",
|
||||
"SUCCESS": "🪄 Setup complete"
|
||||
}
|
||||
},
|
||||
"FOUNDERS_NOTE": {
|
||||
"TITLE": "A note from the founder",
|
||||
"SUBMIT": "Go to dashboard"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,11 @@ import helpcenterRoutes from './helpcenter/helpcenter.routes';
|
||||
|
||||
const AppContainer = () => import('./Dashboard.vue');
|
||||
const Suspended = () => import('./suspended/Index.vue');
|
||||
const RouteLayout = () => import('v3/views/onboarding/RouteLayout.vue');
|
||||
const SetupProfile = () => import('v3/views/onboarding/SetupProfile.vue');
|
||||
const SetupCompany = () => import('v3/views/onboarding/SetupCompany.vue');
|
||||
const InviteTeam = () => import('v3/views/onboarding/InviteTeam.vue');
|
||||
const FoundersNote = () => import('v3/views/onboarding/FoundersNote.vue');
|
||||
|
||||
export default {
|
||||
routes: [
|
||||
@@ -29,5 +34,31 @@ export default {
|
||||
roles: ['administrator', 'agent'],
|
||||
component: Suspended,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/start'),
|
||||
component: RouteLayout,
|
||||
children: [
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/start/setup-profile'),
|
||||
name: 'onboarding_setup_profile',
|
||||
component: SetupProfile,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/start/setup-company'),
|
||||
name: 'onboarding_setup_company',
|
||||
component: SetupCompany,
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/start/invite-team'),
|
||||
name: 'onboarding_invite_team',
|
||||
component: InviteTeam,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: frontendURL('accounts/:accountId/start/founders-note'),
|
||||
name: 'onboarding_founders_note',
|
||||
component: FoundersNote,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -45,6 +45,13 @@ export const validateAuthenticateRoutePermission = (to, next, { getters }) => {
|
||||
return next(frontendURL(`accounts/${user.account_id}/dashboard`));
|
||||
}
|
||||
|
||||
// we can let them access onboarding routes
|
||||
// we will eventually need guards as onboarding is done, but for now
|
||||
// we can let them access onboarding routes
|
||||
if (to.name.startsWith('onboarding')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const nextRoute = validateLoggedInRoutes(
|
||||
to,
|
||||
getters.getCurrentUser,
|
||||
|
||||
@@ -113,6 +113,10 @@
|
||||
"folder-outline": "M8.207 4c.46 0 .908.141 1.284.402l.156.12L12.022 6.5h7.728a2.25 2.25 0 0 1 2.229 1.938l.016.158.005.154v9a2.25 2.25 0 0 1-2.096 2.245L19.75 20H4.25a2.25 2.25 0 0 1-2.245-2.096L2 17.75V6.25a2.25 2.25 0 0 1 2.096-2.245L4.25 4h3.957Zm1.44 5.979a2.25 2.25 0 0 1-1.244.512l-.196.009-4.707-.001v7.251c0 .38.282.694.648.743l.102.007h15.5a.75.75 0 0 0 .743-.648l.007-.102v-9a.75.75 0 0 0-.648-.743L19.75 8h-7.729L9.647 9.979ZM8.207 5.5H4.25a.75.75 0 0 0-.743.648L3.5 6.25v2.749L8.207 9a.75.75 0 0 0 .395-.113l.085-.06 1.891-1.578-1.89-1.575a.75.75 0 0 0-.377-.167L8.207 5.5Z",
|
||||
"globe-desktop-outline": "M22.002 12C22.002 6.477 17.524 2 12 2 6.476 1.999 2 6.477 2 12.001c0 5.186 3.947 9.45 9.001 9.952V20.11c-.778-.612-1.478-1.905-1.939-3.61h1.94V15H8.737a18.969 18.969 0 0 1-.135-5h6.794c.068.64.105 1.31.105 2h1.5c0-.684-.033-1.353-.095-2h3.358c.154.64.237 1.31.237 2h1.5ZM4.786 16.5h2.722l.102.396c.317 1.17.748 2.195 1.27 3.015a8.532 8.532 0 0 1-4.094-3.41ZM3.736 10h3.358a20.847 20.847 0 0 0-.095 2c0 1.043.075 2.051.217 3H4.043a8.483 8.483 0 0 1-.544-3c0-.682.08-1.347.232-1.983L3.736 10Zm5.122-5.902.023-.008C8.16 5.222 7.611 6.748 7.298 8.5H4.25c.905-2 2.56-3.587 4.608-4.402Zm3.026-.594L12 3.5l.126.006c1.262.126 2.48 2.125 3.045 4.995H8.83c.568-2.878 1.79-4.88 3.055-4.996Zm3.343.76-.107-.174.291.121a8.533 8.533 0 0 1 4.339 4.29h-3.048c-.298-1.665-.806-3.125-1.475-4.237Z M12 19a1 1 0 0 0 1 1h3v2h-.5a.5.5 0 1 0 0 1h4a.5.5 0 0 0 0-1H19v-2h3a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v5Z",
|
||||
"globe-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999ZM14.939 16.5H9.06c.652 2.414 1.786 4.002 2.939 4.002s2.287-1.588 2.939-4.002Zm-7.43 0H4.785a8.532 8.532 0 0 0 4.094 3.411c-.522-.82-.953-1.846-1.27-3.015l-.102-.395Zm11.705 0h-2.722c-.324 1.335-.792 2.5-1.373 3.411a8.528 8.528 0 0 0 3.91-3.127l.185-.283ZM7.094 10H3.735l-.005.017a8.525 8.525 0 0 0-.233 1.984c0 1.056.193 2.067.545 3h3.173a20.847 20.847 0 0 1-.123-5Zm8.303 0H8.603a18.966 18.966 0 0 0 .135 5h6.524a18.974 18.974 0 0 0 .135-5Zm4.868 0h-3.358c.062.647.095 1.317.095 2a20.3 20.3 0 0 1-.218 3h3.173a8.482 8.482 0 0 0 .544-3c0-.689-.082-1.36-.236-2ZM8.88 4.09l-.023.008A8.531 8.531 0 0 0 4.25 8.5h3.048c.314-1.752.86-3.278 1.583-4.41ZM12 3.499l-.116.005C10.62 3.62 9.396 5.622 8.83 8.5h6.342c-.566-2.87-1.783-4.869-3.045-4.995L12 3.5Zm3.12.59.107.175c.669 1.112 1.177 2.572 1.475 4.237h3.048a8.533 8.533 0 0 0-4.339-4.29l-.291-.121Z",
|
||||
"globe-clock-outline": [
|
||||
"M22.003 12.002C22.003 6.478 17.525 2 12.002 2 6.478 2 2 6.478 2 12.002c0 5.523 4.478 10.001 10.002 10.001.263 0 .524-.01.781-.03a6.506 6.506 0 0 1-1.065-1.502c-1.053-.235-2.057-1.754-2.655-3.97h2.013c.081-.523.224-1.026.422-1.5H8.74a18.966 18.966 0 0 1-.135-5h6.793c.046.426.077.865.092 1.316a6.429 6.429 0 0 1 1.49-.296 20.38 20.38 0 0 0-.072-1.02H20.266c.124.513.201 1.044.227 1.59l.008.317-.004-.177c.541.281 1.037.637 1.476 1.053.02-.258.03-.52.03-.782Zm-17.216 4.5H7.51c.324 1.334.791 2.5 1.372 3.41a8.531 8.531 0 0 1-4.095-3.41Zm-1.054-6.484.004-.017h3.358A20.845 20.845 0 0 0 7 12c0 1.043.075 2.051.218 3H4.045a8.483 8.483 0 0 1-.545-3c0-.682.08-1.346.233-1.983ZM8.86 4.1l.023-.008C8.16 5.223 7.613 6.749 7.3 8.5H4.252c.904-2 2.56-3.587 4.608-4.402Zm3.026-.594.116-.005c1.306 0 2.586 2.035 3.17 5.001H8.83c.568-2.878 1.79-4.88 3.056-4.996Zm3.342.76-.106-.174a8.531 8.531 0 0 1 4.63 4.41h-3.049c-.297-1.664-.805-3.124-1.475-4.236Z",
|
||||
"M23 17.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0Zm-5.5 0h2a.5.5 0 0 1 0 1H17a.5.5 0 0 1-.5-.491v-3.01a.5.5 0 0 1 1 0V17.5Z"
|
||||
],
|
||||
"headset-add-outline": "M23 6.5C23 3.46243 20.5376 1 17.5 1C14.4624 1 12 3.46243 12 6.5C12 9.53757 14.4624 12 17.5 12C20.5376 12 23 9.53757 23 6.5ZM18.0006 7.00001L18.0011 9.50352C18.0011 9.77966 17.7773 10.0035 17.5011 10.0035C17.225 10.0035 17.0011 9.77966 17.0011 9.50352L17.0006 7.00001H14.4956C14.2197 7.00001 13.9961 6.77615 13.9961 6.50001C13.9961 6.22387 14.2197 6.00001 14.4956 6.00001H17.0005L17 3.49927C17 3.22313 17.2239 2.99927 17.5 2.99927C17.7761 2.99927 18 3.22313 18 3.49927L18.0005 6.00001H20.4966C20.7725 6.00001 20.9961 6.22387 20.9961 6.50001C20.9961 6.77615 20.7725 7.00001 20.4966 7.00001H18.0006ZM17.5 13C18.0163 13 18.5185 12.9398 19 12.8261V15C19 16.0544 18.1841 16.9182 17.1493 16.9945L17 17H15C14.4872 17 14.0645 16.614 14.0067 16.1166L14 16V12L14.0002 11.9783C14.4634 12.2748 14.9666 12.5142 15.5 12.6865V15.5H17C17.2455 15.5 17.4496 15.3231 17.4919 15.0899L17.5 15V13ZM12.0218 3.00003C11.7171 3.47601 11.4727 3.99433 11.2994 4.5442C8.59265 4.88833 6.5 7.19978 6.5 10V11H9C9.55228 11 10 11.4477 10 12V16C10 16.5523 9.55228 17 9 17H6.5V17.25C6.5 18.4409 7.42516 19.4156 8.59595 19.4948L8.75 19.5L9.87812 19.4997C10.1871 18.626 11.0204 18 12 18C13.2426 18 14.25 19.0074 14.25 20.25C14.25 21.4926 13.2426 22.5 12 22.5C11.0201 22.5 10.1864 21.8735 9.87776 20.9992L8.75 21C6.74574 21 5.10873 19.4276 5.0052 17.4492L5 17.25V10C5 6.13401 8.13401 3 12 3L12.0218 3.00003ZM12 19.5C11.5858 19.5 11.25 19.8358 11.25 20.25C11.25 20.6642 11.5858 21 12 21C12.4142 21 12.75 20.6642 12.75 20.25C12.75 19.8358 12.4142 19.5 12 19.5ZM8.5 12.5H6.5V15.5H8.5V12.5Z",
|
||||
"headphones-sound-wave-outline": "M3.5 12a8.5 8.5 0 0 1 17 0v2h-2.25a.75.75 0 0 0-.75.75v6.5c0 .414.336.75.75.75H19a3 3 0 0 0 3-3v-7c0-5.523-4.477-10-10-10S2 6.477 2 12v7a3 3 0 0 0 3 3h.75a.75.75 0 0 0 .75-.75v-6.5a.75.75 0 0 0-.75-.75H3.5v-2Zm17 3.5V19a1.5 1.5 0 0 1-1.5 1.5v-5h1.5ZM3.5 19v-3.5H5v5A1.5 1.5 0 0 1 3.5 19Zm9.25-7.25a.75.75 0 0 0-1.5 0v10.5a.75.75 0 0 0 1.5 0v-10.5Zm-4 2.25a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5a.75.75 0 0 1 .75-.75Zm7.25.75a.75.75 0 0 0-1.5 0v4.5a.75.75 0 0 0 1.5 0v-4.5Z",
|
||||
"image-outline": "M17.75 3A3.25 3.25 0 0 1 21 6.25v11.5A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h11.5Zm.58 16.401-5.805-5.686a.75.75 0 0 0-.966-.071l-.084.07-5.807 5.687c.182.064.378.099.582.099h11.5c.203 0 .399-.035.58-.099l-5.805-5.686L18.33 19.4ZM17.75 4.5H6.25A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .208.036.408.103.594l5.823-5.701a2.25 2.25 0 0 1 3.02-.116l.128.116 5.822 5.702c.067-.186.104-.386.104-.595V6.25a1.75 1.75 0 0 0-1.75-1.75Zm-2.498 2a2.252 2.252 0 1 1 0 4.504 2.252 2.252 0 0 1 0-4.504Zm0 1.5a.752.752 0 1 0 0 1.504.752.752 0 0 0 0-1.504Z",
|
||||
@@ -122,6 +126,7 @@
|
||||
"library-outline": "M4 3h1c1.054 0 1.918.816 1.995 1.85L7 5v14a2.001 2.001 0 0 1-1.85 1.994L5 21H4a2.001 2.001 0 0 1-1.995-1.85L2 19V5c0-1.054.816-1.918 1.85-1.995L4 3h1-1Zm6 0h1c1.054 0 1.918.816 1.995 1.85L13 5v14a2.001 2.001 0 0 1-1.85 1.994L11 21h-1a2.001 2.001 0 0 1-1.995-1.85L8 19V5c0-1.054.816-1.918 1.85-1.995L10 3h1-1Zm6.974 2c.84 0 1.608.531 1.89 1.346l.047.157 3.015 11.745a2 2 0 0 1-1.296 2.392l-.144.043-.969.248a2.002 2.002 0 0 1-2.387-1.284l-.047-.155-3.016-11.745a2 2 0 0 1 1.298-2.392l.143-.043.968-.248c.166-.043.334-.064.498-.064ZM5 4.5H4a.501.501 0 0 0-.492.41L3.5 5v14c0 .244.177.45.41.492L4 19.5h1c.245 0 .45-.178.492-.41L5.5 19V5a.501.501 0 0 0-.41-.492L5 4.5Zm6 0h-1a.501.501 0 0 0-.492.41L9.5 5v14c0 .244.177.45.41.492l.09.008h1c.245 0 .45-.178.492-.41L11.5 19V5a.501.501 0 0 0-.41-.492L11 4.5Zm5.975 2-.063.004-.063.013-.968.247a.498.498 0 0 0-.376.51l.015.1 3.016 11.745a.5.5 0 0 0 .483.375l.063-.003.062-.012.97-.25a.5.5 0 0 0 .374-.519l-.015-.088-3.015-11.747a.501.501 0 0 0-.483-.375Z",
|
||||
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
|
||||
"list-outline": "M2.75 18h12.5a.75.75 0 0 1 .102 1.493l-.102.007H2.75a.75.75 0 0 1-.102-1.494L2.75 18h12.5-12.5Zm0-6.5h18.5a.75.75 0 0 1 .102 1.493L21.25 13H2.75a.75.75 0 0 1-.102-1.493l.102-.007h18.5-18.5Zm0-6.497h15.5a.75.75 0 0 1 .102 1.493l-.102.007H2.75a.75.75 0 0 1-.102-1.493l.102-.007h15.5-15.5Z",
|
||||
"local-language-outline": "m9.34 6.372.05.105 5.56 14.5a.755.755 0 0 1-.418.971.73.73 0 0 1-.91-.333l-.043-.092-1.433-3.737H5.06l-.094-.006-1.546 3.76a.73.73 0 0 1-.963.401.754.754 0 0 1-.427-.885l.033-.096 5.964-14.5a.73.73 0 0 1 1.314-.088Zm9.406-4.37a.75.75 0 0 1 .743.65l.007.1V7.5h1.75a.75.75 0 0 1 .743.649l.007.102a.75.75 0 0 1-.648.743L21.245 9l-1.75-.001v7.25a.75.75 0 0 1-.648.744l-.102.007a.75.75 0 0 1-.743-.648l-.007-.102V2.753a.75.75 0 0 1 .75-.75ZM8.81 8.748 5.65 16.286h6.11L8.81 8.747Zm1.937-6.744h5.498a.75.75 0 0 1 .743.648l.006.102v3.004c0 2.344-1.9 4.245-4.245 4.245a.75.75 0 0 1 0-1.5c1.46 0 2.654-1.14 2.74-2.578l.005-.167V3.503h-4.747a.75.75 0 0 1-.102-1.493l.102-.007h5.498-5.498Z",
|
||||
"location-outline": "M5.843 4.568a8.707 8.707 0 1 1 12.314 12.314l-1.187 1.174c-.875.858-2.01 1.962-3.406 3.312a2.25 2.25 0 0 1-3.128 0l-3.491-3.396c-.439-.431-.806-.794-1.102-1.09a8.707 8.707 0 0 1 0-12.314Zm11.253 1.06A7.207 7.207 0 1 0 6.904 15.822L8.39 17.29a753.98 753.98 0 0 0 3.088 3 .75.75 0 0 0 1.043 0l3.394-3.3c.47-.461.863-.85 1.18-1.168a7.207 7.207 0 0 0 0-10.192ZM12 7.999a3.002 3.002 0 1 1 0 6.004 3.002 3.002 0 0 1 0-6.003Zm0 1.5a1.501 1.501 0 1 0 0 3.004 1.501 1.501 0 0 0 0-3.003Z",
|
||||
"lock-closed-outline": "M12 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 20 10.25v9.5A2.25 2.25 0 0 1 17.75 22H6.25A2.25 2.25 0 0 1 4 19.75v-9.5A2.25 2.25 0 0 1 6.25 8H8V6a4 4 0 0 1 4-4Zm5.75 7.5H6.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h11.5a.75.75 0 0 0 .75-.75v-9.5a.75.75 0 0 0-.75-.75Zm-5.75 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 9.5 6v2h5V6A2.5 2.5 0 0 0 12 3.5Z",
|
||||
"lock-shield-outline": "M10 2a4 4 0 0 1 4 4v2h1.75A2.25 2.25 0 0 1 18 10.25V11c-.319 0-.637.11-.896.329l-.107.1c-.164.17-.33.323-.496.457L16.5 10.25a.75.75 0 0 0-.75-.75H4.25a.75.75 0 0 0-.75.75v9.5c0 .414.336.75.75.75h9.888a6.024 6.024 0 0 0 1.54 1.5H4.25A2.25 2.25 0 0 1 2 19.75v-9.5A2.25 2.25 0 0 1 4.25 8H6V6a4 4 0 0 1 4-4Zm8.284 10.122c.992 1.036 2.091 1.545 3.316 1.545.193 0 .355.143.392.332l.008.084v2.501c0 2.682-1.313 4.506-3.873 5.395a.385.385 0 0 1-.253 0c-2.476-.86-3.785-2.592-3.87-5.13L14 16.585v-2.5c0-.23.18-.417.4-.417 1.223 0 2.323-.51 3.318-1.545a.389.389 0 0 1 .566 0ZM10 13.5a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm0-10A2.5 2.5 0 0 0 7.5 6v2h5V6A2.5 2.5 0 0 0 10 3.5Z",
|
||||
@@ -192,6 +197,7 @@
|
||||
"M2.75 17h7.775l-1.5 1.5H2.75a.75.75 0 0 1-.102-1.493L2.75 17ZM2.75 14.5h7.564a1.745 1.745 0 0 1-.241-1.5H2.75l-.102.007A.75.75 0 0 0 2.75 14.5ZM12.72 16.22a.75.75 0 1 1 1.06 1.06l-4.5 4.5a.75.75 0 0 1-1.06-1.06l4.5-4.5ZM2.75 9h10.5a.75.75 0 0 1 .102 1.493l-.102.007H2.75a.75.75 0 0 1-.102-1.493L2.75 9ZM2.75 5h18.5a.75.75 0 0 1 .102 1.493l-.102.007H2.75a.75.75 0 0 1-.102-1.493L2.75 5Z",
|
||||
"M16.5 8a.75.75 0 0 1 .744.658l.14 1.13a3.25 3.25 0 0 0 2.828 2.829l1.13.139a.75.75 0 0 1 0 1.488l-1.13.14a3.25 3.25 0 0 0-2.829 2.828l-.139 1.13a.75.75 0 0 1-1.488 0l-.14-1.13a3.25 3.25 0 0 0-2.828-2.829l-1.13-.139a.75.75 0 0 1 0-1.488l1.13-.14a3.25 3.25 0 0 0 2.829-2.828l.139-1.13A.75.75 0 0 1 16.5 8Zm0 3.774a4.761 4.761 0 0 1-1.726 1.726 4.761 4.761 0 0 1 1.726 1.726 4.761 4.761 0 0 1 1.726-1.726 4.761 4.761 0 0 1-1.726-1.726Z"
|
||||
],
|
||||
"toolbox-outline": "M7 6.252V7H4.25A2.25 2.25 0 0 0 2 9.25v8.497a2.25 2.25 0 0 0 2.25 2.25h15.5a2.25 2.25 0 0 0 2.25-2.25V9.25A2.25 2.25 0 0 0 19.75 7H17v-.748a2.25 2.25 0 0 0-2.25-2.25h-5.5A2.25 2.25 0 0 0 7 6.252Zm2.25-.75h5.5a.75.75 0 0 1 .75.75V7h-7v-.748a.75.75 0 0 1 .75-.75Zm-2.25 3h1.5V8.5h7v.002H17V8.5h2.75a.75.75 0 0 1 .75.75v2.25h-3v-.75a.75.75 0 0 0-1.5 0v.75H8v-.75a.75.75 0 0 0-1.5 0v.75h-3V9.25a.75.75 0 0 1 .75-.75H7v.002ZM16 13v1.25a.75.75 0 0 0 1.5 0V13h3v4.747a.75.75 0 0 1-.75.75H4.25a.75.75 0 0 1-.75-.75V13h3v1.25a.75.75 0 0 0 1.5 0V13h8Z",
|
||||
"translate-outline": "M16.953 5.303a1 1 0 0 0-1.906-.606c-.124.389-.236.899-.324 1.344-.565.012-1.12 0-1.652-.038a1 1 0 1 0-.142 1.995c.46.032.934.048 1.416.047a25.649 25.649 0 0 0-.24 1.698c-1.263.716-2.142 1.684-2.636 2.7-.624 1.283-.7 2.857.239 3.883.675.736 1.704.758 2.499.588.322-.068.654-.176.988-.32a1 1 0 0 0 1.746-.93 13.17 13.17 0 0 0-.041-.115 8.404 8.404 0 0 0 2.735-4.06c.286.251.507.55.658.864.284.594.334 1.271.099 1.91-.234.633-.78 1.313-1.84 1.843a1 1 0 0 0 .895 1.789c1.44-.72 2.385-1.758 2.821-2.94a4.436 4.436 0 0 0-.17-3.464 4.752 4.752 0 0 0-2.104-2.165C19.998 9.22 20 9.11 20 9a1 1 0 0 0-1.974-.23 5.984 5.984 0 0 0-1.796.138c.047-.305.102-.626.166-.964a20.142 20.142 0 0 0 2.842-.473 1 1 0 0 0-.476-1.942c-.622.152-1.286.272-1.964.358.048-.208.1-.409.155-.584Zm-3.686 8.015c.166-.34.414-.697.758-1.037.02.348.053.67.098.973.083.56.207 1.048.341 1.477a3.41 3.41 0 0 1-.674.227c-.429.092-.588.019-.614.006l-.004-.001c-.162-.193-.329-.774.095-1.645Zm4.498-2.562a6.362 6.362 0 0 1-1.568 2.73 7.763 7.763 0 0 1-.095-.525 10.294 10.294 0 0 1-.088-1.904c.033-.013.067-.024.1-.036l1.651-.265Zm0 0-1.651.265c.602-.212 1.155-.29 1.651-.265ZM7.536 6.29a6.342 6.342 0 0 0-4.456.331 1 1 0 0 0 .848 1.811 4.342 4.342 0 0 1 3.049-.222c.364.107.568.248.69.37.12.123.203.27.257.454.067.225.087.446.09.69a8.195 8.195 0 0 0-.555-.117c-1.146-.199-2.733-.215-4.262.64-1.271.713-1.796 2.168-1.682 3.448.12 1.326.94 2.679 2.572 3.136 1.48.414 2.913-.045 3.877-.507l.08-.04a1 1 0 0 0 1.96-.281V10.5c0-.053.002-.12.005-.2.012-.417.034-1.16-.168-1.838a3.043 3.043 0 0 0-.755-1.29c-.394-.398-.91-.694-1.547-.881h-.003Zm-.419 5.288c.344.06.647.143.887.222v2.197a7.021 7.021 0 0 1-.905.524c-.792.38-1.682.605-2.473.384-.698-.195-1.06-.742-1.119-1.389-.062-.693.243-1.286.667-1.523.987-.553 2.06-.569 2.943-.415Z",
|
||||
"eye-show-outline": "M12 9.005a4 4 0 1 1 0 8 4 4 0 0 1 0-8Zm0 1.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5ZM12 5.5c4.613 0 8.596 3.15 9.701 7.564a.75.75 0 1 1-1.455.365 8.503 8.503 0 0 0-16.493.004.75.75 0 0 1-1.455-.363A10.003 10.003 0 0 1 12 5.5Z",
|
||||
"eye-hide-outline": "M2.22 2.22a.75.75 0 0 0-.073.976l.073.084 4.034 4.035a9.986 9.986 0 0 0-3.955 5.75.75.75 0 0 0 1.455.364 8.49 8.49 0 0 1 3.58-5.034l1.81 1.81A4 4 0 0 0 14.8 15.86l5.919 5.92a.75.75 0 0 0 1.133-.977l-.073-.084-6.113-6.114.001-.002-1.2-1.198-2.87-2.87h.002L8.719 7.658l.001-.002-1.133-1.13L3.28 2.22a.75.75 0 0 0-1.06 0Zm7.984 9.045 3.535 3.536a2.5 2.5 0 0 1-3.535-3.535ZM12 5.5c-1 0-1.97.148-2.889.425l1.237 1.236a8.503 8.503 0 0 1 9.899 6.272.75.75 0 0 0 1.455-.363A10.003 10.003 0 0 0 12 5.5Zm.195 3.51 3.801 3.8a4.003 4.003 0 0 0-3.801-3.8Z",
|
||||
|
||||
@@ -27,9 +27,7 @@ export const login = async ({
|
||||
|
||||
export const register = async creds => {
|
||||
try {
|
||||
const response = await wootAPI.post('api/v1/accounts.json', {
|
||||
account_name: creds.accountName.trim(),
|
||||
user_full_name: creds.fullName.trim(),
|
||||
const response = await wootAPI.post('api/v2/accounts.json', {
|
||||
email: creds.email,
|
||||
password: creds.password,
|
||||
h_captcha_client_response: creds.hCaptchaClientResponse,
|
||||
@@ -42,6 +40,20 @@ export const register = async creds => {
|
||||
return null;
|
||||
};
|
||||
|
||||
export const accountSetup = async ({ id, name, locale }) => {
|
||||
try {
|
||||
const response = await wootAPI.put(`api/v2/accounts/${id}.json`, {
|
||||
name,
|
||||
locale,
|
||||
});
|
||||
// setAuthCredentials(response);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throwErrorMessage(error);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const verifyPasswordToken = async ({ confirmationToken }) => {
|
||||
try {
|
||||
const response = await wootAPI.post('auth/confirmation', {
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
'px-3 py-2 mb-0': spacing === 'compact',
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="block w-full border-none shadow-sm appearance-none rounded-xl outline outline-1 focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
class="block w-full border-none shadow-sm appearance-none rounded-md outline outline-1 focus:outline focus:outline-2 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
:variant="value === option.value ? '' : 'hollow'"
|
||||
:color-scheme="value === option.value ? 'primary' : 'secondary'"
|
||||
size="small"
|
||||
type=""
|
||||
@click="$emit('input', option.value)"
|
||||
>
|
||||
{{ option.label }}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
'text-slate-900 dark:text-slate-100': value,
|
||||
'pl-9': icon,
|
||||
}"
|
||||
class="mb-0 block w-full px-3 py-2 pr-6 border-0 rounded-xl shadow-sm outline-none appearance-none select-caret ring-1 ring-inset placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-700 dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200"
|
||||
class="mb-0 block w-full px-3 py-2 pr-6 border-0 rounded-md shadow-sm outline-none appearance-none select-caret ring-1 ring-inset placeholder:text-slate-400 focus:ring-2 focus:ring-inset focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-700 dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200"
|
||||
@input="onInput"
|
||||
>
|
||||
<option value="" disabled selected class="hidden">
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<with-label
|
||||
:label="label"
|
||||
:name="name"
|
||||
:has-error="hasError"
|
||||
:error-message="errorMessage"
|
||||
>
|
||||
<textarea
|
||||
:id="name"
|
||||
:name="name"
|
||||
autocomplete="off"
|
||||
:required="required"
|
||||
:placeholder="placeholder"
|
||||
:data-testid="dataTestid"
|
||||
:value="value"
|
||||
:rows="rows"
|
||||
:class="{
|
||||
'focus:ring-red-600 ring-red-600': hasError,
|
||||
'dark:ring-slate-600 dark:focus:ring-woot-500 ring-slate-200':
|
||||
!hasError,
|
||||
'px-3 py-3': spacing === 'base',
|
||||
'px-3 py-2': spacing === 'compact',
|
||||
'resize-none': !allowResize,
|
||||
}"
|
||||
class="block w-full border-none rounded-md shadow-sm appearance-none outline outline-1 outline-slate-200 dark:outline-slate-800 focus:outline-none focus:outline-0 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:ring-2 focus:ring-woot-500 sm:text-sm sm:leading-6 dark:bg-slate-800"
|
||||
@input="onInput"
|
||||
@blur="$emit('blur')"
|
||||
/>
|
||||
</with-label>
|
||||
</template>
|
||||
<script>
|
||||
import WithLabel from './WithLabel.vue';
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
},
|
||||
props: {
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
default: '',
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 3,
|
||||
},
|
||||
allowResize: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
hasError: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
errorMessage: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
dataTestid: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
spacing: {
|
||||
type: String,
|
||||
default: 'base',
|
||||
validator: value => ['base', 'compact'].includes(value),
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onInput(e) {
|
||||
this.$emit('input', e.target.value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,13 @@
|
||||
export const getBrowserLocale = enabledLanguages => {
|
||||
const localeWithVariant = window.navigator.language.replace('-', '_');
|
||||
const localeWithoutVariant = localeWithVariant.split('_')[0];
|
||||
return enabledLanguages.find(
|
||||
lang =>
|
||||
lang.iso_639_1_code === localeWithVariant ||
|
||||
lang.iso_639_1_code === localeWithoutVariant
|
||||
)?.iso_639_1_code;
|
||||
};
|
||||
|
||||
export const getBrowserTimezone = () => {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
export const findMatchingOption = (value, options, defaultValue) => {
|
||||
const match = options.find(
|
||||
option => option.value === value || option === value
|
||||
);
|
||||
return match ? match.value || match : defaultValue;
|
||||
};
|
||||
|
||||
export const findIndustryOptions = subIndustry => {
|
||||
const industryMap = {
|
||||
'Information Technology': [
|
||||
'Semiconductors & Semiconductor Equipment',
|
||||
'Internet Software & Services',
|
||||
'IT Services',
|
||||
'Software',
|
||||
'Communications Equipment',
|
||||
'Electronic Equipment, Instruments & Components',
|
||||
'Technology Hardware, Storage & Peripherals',
|
||||
],
|
||||
Financials: [
|
||||
'Banks',
|
||||
'Diversified Financial Services',
|
||||
'Capital Markets',
|
||||
'Insurance',
|
||||
],
|
||||
'Health & Medicine': [
|
||||
'Health Care Equipment & Supplies',
|
||||
'Health Care Providers & Services',
|
||||
'Biotechnology',
|
||||
'Life Sciences Tools & Services',
|
||||
'Pharmaceuticals',
|
||||
],
|
||||
Education: ['Education Services'],
|
||||
};
|
||||
|
||||
const industry = Object.entries(industryMap).find(([, value]) =>
|
||||
value.includes(subIndustry)
|
||||
);
|
||||
|
||||
return industry ? industry[0] : 'Other';
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { getBrowserLocale, getBrowserTimezone } from '../BrowserHelper';
|
||||
|
||||
describe('getBrowserLocale', () => {
|
||||
it('should return the correct locale code when an exact match is found', () => {
|
||||
// Mock window.navigator.language
|
||||
Object.defineProperty(window.navigator, 'language', {
|
||||
value: 'en-US',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const languages = [{ iso_639_1_code: 'en' }, { iso_639_1_code: 'en_US' }];
|
||||
expect(getBrowserLocale(languages)).toBe('en');
|
||||
});
|
||||
|
||||
it('should return the correct locale code when only a partial match is found', () => {
|
||||
Object.defineProperty(window.navigator, 'language', {
|
||||
value: 'en-GB',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const languages = [{ iso_639_1_code: 'en' }, { iso_639_1_code: 'fr' }];
|
||||
expect(getBrowserLocale(languages)).toBe('en');
|
||||
});
|
||||
|
||||
it('should return undefined when no match is found', () => {
|
||||
Object.defineProperty(window.navigator, 'language', {
|
||||
value: 'es-ES',
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
const languages = [{ iso_639_1_code: 'en' }, { iso_639_1_code: 'fr' }];
|
||||
expect(getBrowserLocale(languages)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBrowserTimezone', () => {
|
||||
it('should return the current browser timezone', () => {
|
||||
// This test assumes the environment's timezone is set.
|
||||
// It's a basic test as getBrowserTimezone relies on Intl API.
|
||||
const timezone = getBrowserTimezone();
|
||||
expect(timezone).toBe(Intl.DateTimeFormat().resolvedOptions().timeZone);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { findMatchingOption, findIndustryOptions } from '../OnboardingHelper';
|
||||
|
||||
describe('findMatchingOption', () => {
|
||||
const options = [{ value: 'option1' }, { value: 'option2' }];
|
||||
|
||||
it('should return the matching option', () => {
|
||||
expect(findMatchingOption('option1', options, 'default')).toBe('option1');
|
||||
});
|
||||
|
||||
it('should return the default value when no match is found', () => {
|
||||
expect(findMatchingOption('nonExistingOption', options, 'default')).toBe(
|
||||
'default'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findIndustryOptions', () => {
|
||||
it('should return the correct industry for a given sub-industry', () => {
|
||||
expect(
|
||||
findIndustryOptions('Semiconductors & Semiconductor Equipment')
|
||||
).toBe('Information Technology');
|
||||
expect(findIndustryOptions('Banks')).toBe('Financials');
|
||||
expect(findIndustryOptions('Health Care Equipment & Supplies')).toBe(
|
||||
'Health & Medicine'
|
||||
);
|
||||
expect(findIndustryOptions('Education Services')).toBe('Education');
|
||||
});
|
||||
|
||||
it('should return "Other" for a sub-industry not in the map', () => {
|
||||
expect(findIndustryOptions('Non-existing Sub-industry')).toBe('Other');
|
||||
});
|
||||
|
||||
it('should return "Other" for an empty string', () => {
|
||||
expect(findIndustryOptions('')).toBe('Other');
|
||||
});
|
||||
|
||||
it('should return "Other" for a null value', () => {
|
||||
expect(findIndustryOptions(null)).toBe('Other');
|
||||
});
|
||||
});
|
||||
@@ -1,24 +1,56 @@
|
||||
<template>
|
||||
<div class="h-full w-full dark:bg-slate-900">
|
||||
<div v-show="!isLoading" class="flex h-full">
|
||||
<div class="flex h-full w-full dark:bg-slate-900 relative overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-0 h-full w-full bg-white dark:bg-slate-900 bg-[radial-gradient(var(--w-100)_1px,transparent_1px)] dark:bg-[radial-gradient(var(--w-800)_1px,transparent_1px)] [background-size:16px_16px] z-0"
|
||||
/>
|
||||
<div
|
||||
class="absolute h-full w-full overlay-gradient top-0 left-0 blur-[3px]"
|
||||
/>
|
||||
|
||||
<svg
|
||||
width="528"
|
||||
height="96"
|
||||
viewBox="0 0 528 96"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="absolute -left-full -bottom-full"
|
||||
>
|
||||
<defs>
|
||||
<mask id="cutoutMask">
|
||||
<path
|
||||
d="M263.999 65.0769C293.048 65.0769 317.641 45.9499 325.929 19.5735C329.219 9.10348 337.794 0 348.744 0H508.172C519.122 0 527.999 8.89875 527.999 19.8759V95.5C527.999 95.5 519.122 95.5 508.172 95.5H263.999H-0.000976562V19.8759C-0.000976562 8.89877 8.87604 0 19.8264 0H179.254C190.204 0 198.779 9.10349 202.069 19.5735C210.357 45.9499 234.95 65.0769 263.999 65.0769Z"
|
||||
fill="white"
|
||||
/>
|
||||
</mask>
|
||||
</defs>
|
||||
</svg>
|
||||
|
||||
<div
|
||||
class="flex w-full h-full relative z-50 items-center justify-center overflow-auto"
|
||||
>
|
||||
<div
|
||||
class="flex-1 min-h-[640px] inline-flex items-center h-full justify-center overflow-auto py-6"
|
||||
class="relative flex-1 min-h-[640px] inline-flex items-center h-full justify-center py-6"
|
||||
>
|
||||
<div class="px-8 max-w-[560px] w-full overflow-auto">
|
||||
<div class="mb-4">
|
||||
<div
|
||||
class="relative bg-white dark:bg-slate-800 before:bg-white before:border-slate-200 dark:before:border-slate-700 dark:before:bg-slate-800 px-16 pt-8 pb-16 max-w-[528px] w-full rounded-3xl signup-box"
|
||||
>
|
||||
<div class="absolute -top-[116px] right-0 left-0 w-24 h-24 mx-auto">
|
||||
<img
|
||||
:src="globalConfig.logo"
|
||||
:src="globalConfig.logoThumbnail"
|
||||
:alt="globalConfig.installationName"
|
||||
class="h-8 w-auto block dark:hidden"
|
||||
class="h-24 w-24 block"
|
||||
:class="{ 'dark:hidden': globalConfig.logoDarkThumbnail }"
|
||||
/>
|
||||
<img
|
||||
v-if="globalConfig.logoDark"
|
||||
:src="globalConfig.logoDark"
|
||||
v-if="globalConfig.logoDarkThumbnail"
|
||||
:src="globalConfig.logoDarkThumbnail"
|
||||
:alt="globalConfig.installationName"
|
||||
class="h-8 w-auto hidden dark:block"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-8">
|
||||
<h2
|
||||
class="mb-7 mt-6 text-left text-3xl font-medium text-slate-900 dark:text-woot-50"
|
||||
class="text-center text-3xl tracking-wide font-medium text-slate-900 dark:text-woot-50"
|
||||
>
|
||||
{{ $t('REGISTER.TRY_WOOT') }}
|
||||
</h2>
|
||||
@@ -37,17 +69,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<testimonials
|
||||
v-if="isAChatwootInstance"
|
||||
class="flex-1"
|
||||
@resize-containers="resizeContainers"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-show="isLoading"
|
||||
class="flex items-center justify-center h-full w-full"
|
||||
>
|
||||
<spinner color-scheme="primary" size="" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -56,32 +77,46 @@
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import SignupForm from './components/Signup/Form.vue';
|
||||
import Testimonials from './components/Testimonials/Index.vue';
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SignupForm,
|
||||
Spinner,
|
||||
Testimonials,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
data() {
|
||||
return { isLoading: false };
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
isAChatwootInstance() {
|
||||
return this.globalConfig.installationName === 'Chatwoot';
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
this.isLoading = this.isAChatwootInstance;
|
||||
},
|
||||
methods: {
|
||||
resizeContainers() {
|
||||
this.isLoading = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.overlay-gradient {
|
||||
background: linear-gradient(90deg, rgba(252, 252, 253, 0) 81.8%, #fcfcfd 95%),
|
||||
linear-gradient(270deg, rgba(252, 252, 253, 0) 76.93%, #fcfcfd 95%),
|
||||
linear-gradient(0deg, rgba(252, 252, 253, 0) 68.63%, #fcfcfd 95%),
|
||||
linear-gradient(180deg, rgba(252, 252, 253, 0) 73.2%, #fcfcfd 95%);
|
||||
}
|
||||
.dark .overlay-gradient {
|
||||
background: linear-gradient(270deg, rgba(24, 24, 26, 0) 76.93%, #151718 95%),
|
||||
linear-gradient(90deg, rgba(24, 24, 26, 0) 81.8%, #151718 95%),
|
||||
linear-gradient(0deg, rgba(24, 24, 26, 0) 68.63%, #151718 95%),
|
||||
linear-gradient(180deg, rgba(24, 24, 26, 0) 73.2%, #151718 95%);
|
||||
}
|
||||
.signup-box::before {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
content: '';
|
||||
display: block;
|
||||
left: 0;
|
||||
top: -66px;
|
||||
position: absolute;
|
||||
-webkit-mask-image: url('#cutoutMask');
|
||||
mask-image: url('#cutoutMask');
|
||||
}
|
||||
|
||||
.dark .signup-box {
|
||||
-webkit-filter: drop-shadow(1px 1px 0 var(--s-800))
|
||||
drop-shadow(-1px -1px 0 var(--s-800));
|
||||
filter: drop-shadow(1px 1px 0 var(--s-800))
|
||||
drop-shadow(-1px -1px 0 var(--s-800));
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,52 +1,30 @@
|
||||
<template>
|
||||
<div class="flex-1 overflow-auto px-1">
|
||||
<form class="space-y-3" @submit.prevent="submit">
|
||||
<div class="flex">
|
||||
<div class="flex-1 px-1 overflow-auto">
|
||||
<form class="space-y-8" @submit.prevent="submit">
|
||||
<section class="space-y-3">
|
||||
<form-input
|
||||
v-model.trim="credentials.fullName"
|
||||
name="full_name"
|
||||
class="flex-1"
|
||||
:class="{ error: $v.credentials.fullName.$error }"
|
||||
:label="$t('REGISTER.FULL_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.FULL_NAME.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.fullName.$error"
|
||||
:error-message="$t('REGISTER.FULL_NAME.ERROR')"
|
||||
@blur="$v.credentials.fullName.$touch"
|
||||
v-model.trim="credentials.email"
|
||||
type="email"
|
||||
name="email_address"
|
||||
:class="{ error: $v.credentials.email.$error }"
|
||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.email.$error"
|
||||
:error-message="$t('REGISTER.EMAIL.ERROR')"
|
||||
@blur="$v.credentials.email.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model.trim="credentials.accountName"
|
||||
name="account_name"
|
||||
class="flex-1 ml-2"
|
||||
:class="{ error: $v.credentials.accountName.$error }"
|
||||
:label="$t('REGISTER.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('REGISTER.COMPANY_NAME.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.accountName.$error"
|
||||
:error-message="$t('REGISTER.COMPANY_NAME.ERROR')"
|
||||
@blur="$v.credentials.accountName.$touch"
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
name="password"
|
||||
:class="{ error: $v.credentials.password.$error }"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.password.$error"
|
||||
:error-message="passwordErrorText"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
</div>
|
||||
<form-input
|
||||
v-model.trim="credentials.email"
|
||||
type="email"
|
||||
name="email_address"
|
||||
:class="{ error: $v.credentials.email.$error }"
|
||||
:label="$t('REGISTER.EMAIL.LABEL')"
|
||||
:placeholder="$t('REGISTER.EMAIL.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.email.$error"
|
||||
:error-message="$t('REGISTER.EMAIL.ERROR')"
|
||||
@blur="$v.credentials.email.$touch"
|
||||
/>
|
||||
<form-input
|
||||
v-model.trim="credentials.password"
|
||||
type="password"
|
||||
name="password"
|
||||
:class="{ error: $v.credentials.password.$error }"
|
||||
:label="$t('LOGIN.PASSWORD.LABEL')"
|
||||
:placeholder="$t('SET_NEW_PASSWORD.PASSWORD.PLACEHOLDER')"
|
||||
:has-error="$v.credentials.password.$error"
|
||||
:error-message="passwordErrorText"
|
||||
@blur="$v.credentials.password.$touch"
|
||||
/>
|
||||
</section>
|
||||
<div v-if="globalConfig.hCaptchaSiteKey" class="mb-3">
|
||||
<vue-hcaptcha
|
||||
ref="hCaptcha"
|
||||
@@ -83,15 +61,17 @@ import { required, minLength, email } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import { DEFAULT_REDIRECT_URL } from 'dashboard/constants/globals';
|
||||
import VueHcaptcha from '@hcaptcha/vue-hcaptcha';
|
||||
import FormInput from '../../../../../components/Form/Input.vue';
|
||||
import FormInput from 'v3/components/Form/Input.vue';
|
||||
import SubmitButton from '../../../../../components/Button/SubmitButton.vue';
|
||||
import { isValidPassword } from 'shared/helpers/Validators';
|
||||
import GoogleOAuthButton from '../../../../../components/GoogleOauth/Button.vue';
|
||||
import { register } from '../../../../../api/auth';
|
||||
var CompanyEmailValidator = require('company-email-validator');
|
||||
|
||||
export const generateOnboardingUrl = accountId =>
|
||||
`/app/accounts/${accountId}/start/setup-profile`;
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormInput,
|
||||
@@ -103,10 +83,8 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
credentials: {
|
||||
accountName: '',
|
||||
fullName: '',
|
||||
email: '',
|
||||
password: '',
|
||||
email: 'Testing123@shivam.dev',
|
||||
password: 'Testing123@shivam.dev',
|
||||
hCaptchaClientResponse: '',
|
||||
},
|
||||
didCaptchaReset: false,
|
||||
@@ -116,14 +94,6 @@ export default {
|
||||
},
|
||||
validations: {
|
||||
credentials: {
|
||||
accountName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
fullName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
email: {
|
||||
required,
|
||||
email,
|
||||
@@ -180,8 +150,11 @@ export default {
|
||||
}
|
||||
this.isSignupInProgress = true;
|
||||
try {
|
||||
await register(this.credentials);
|
||||
window.location = DEFAULT_REDIRECT_URL;
|
||||
const {
|
||||
data: { account_id: accountId },
|
||||
} = await register(this.credentials);
|
||||
|
||||
window.location = generateOnboardingUrl(accountId);
|
||||
} catch (error) {
|
||||
let errorMessage =
|
||||
error?.message || this.$t('REGISTER.API.ERROR_MESSAGE');
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="h-full w-full dark:bg-slate-900 relative overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-0 h-full w-full bg-white dark:bg-slate-900 bg-[radial-gradient(var(--w-100)_1px,transparent_1px)] [background-size:16px_16px] z-0"
|
||||
/>
|
||||
<div
|
||||
class="absolute h-full w-full overlay-gradient top-0 left-0 scale-y-110 blur-[3px]"
|
||||
/>
|
||||
<div class="flex h-full relative z-50">
|
||||
<div
|
||||
class="flex-1 min-h-[640px] inline-flex items-center h-full justify-center overflow-auto py-6"
|
||||
>
|
||||
<div
|
||||
class="p-10 max-w-[560px] w-full overflow-auto bg-white dark:bg-slate-900 border border-solid border-slate-100 dark:border-slate-800 rounded-2xl"
|
||||
>
|
||||
<section
|
||||
class="mt-4 space-y-6 text-sm leading-relaxed text-slate-900 dark:text-white"
|
||||
>
|
||||
<div
|
||||
class="rounded-full h-16 w-16 border border-solid border-woot-500 p-4"
|
||||
>
|
||||
<div
|
||||
class="bg-[url('/assets/images/dashboard/onboarding/waving-hand.svg')] bg-contain w-8 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p
|
||||
class="uppercase tracking-wide text-slate-900 dark:text-white mb-1"
|
||||
>
|
||||
a note from the founder
|
||||
</p>
|
||||
<h3 class="text-slate-900 dark:text-white text-3xl font-semibold">
|
||||
Hey {{ userName }},
|
||||
</h3>
|
||||
</div>
|
||||
<p>
|
||||
I'm Pranav, one of the co-founders at Chatwoot. Thanks for
|
||||
checking out Chatwoot. We're eager to understand your needs and
|
||||
expectations.
|
||||
</p>
|
||||
<p>
|
||||
If you are facing trouble using the app or achieving your use
|
||||
case, we want to help you as quickly as possible.
|
||||
</p>
|
||||
<p>
|
||||
Feel free to schedule a call with our team. We are open to your
|
||||
feedback. Talk soon!
|
||||
</p>
|
||||
</section>
|
||||
<figure
|
||||
class="mt-5 text-sm leading-relaxed text-slate-900 dark:text-white"
|
||||
>
|
||||
<woot-thumbnail
|
||||
src="https://www.chatwoot.com/images/team/pranav.jpg"
|
||||
username="Pranav"
|
||||
/>
|
||||
<p class="mt-2">Pranav, <br />Co-founder & CEO, Chatwoot</p>
|
||||
</figure>
|
||||
|
||||
<submit-button
|
||||
button-class="flex justify-center w-full text-sm text-center mt-8"
|
||||
:button-text="$t('START_ONBOARDING.FOUNDERS_NOTE.SUBMIT')"
|
||||
@click="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SubmitButton,
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
userName() {
|
||||
return this.currentUser.name;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onSubmit() {
|
||||
this.$router.push({ name: 'home' });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.overlay-gradient {
|
||||
background: linear-gradient(90deg, rgba(252, 252, 253, 0) 61.8%, #fcfcfd 90%),
|
||||
linear-gradient(270deg, rgba(252, 252, 253, 0) 66.93%, #fcfcfd 90%),
|
||||
linear-gradient(0deg, rgba(252, 252, 253, 0) 68.63%, #fcfcfd 90%),
|
||||
linear-gradient(180deg, rgba(252, 252, 253, 0) 63.2%, #fcfcfd 80%);
|
||||
}
|
||||
.dark .overlay-gradient {
|
||||
background: linear-gradient(270deg, rgba(24, 24, 26, 0) 61.93%, #151718 90%),
|
||||
linear-gradient(90deg, rgba(24, 24, 26, 0) 66.8%, #151718 90%),
|
||||
linear-gradient(0deg, rgba(24, 24, 26, 0) 68.63%, #151718 90%),
|
||||
linear-gradient(180deg, rgba(24, 24, 26, 0) 63.2%, #151718 90%);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,144 @@
|
||||
<template>
|
||||
<div
|
||||
id="modal-body"
|
||||
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative w-full max-w-[560px]"
|
||||
>
|
||||
<h2
|
||||
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.INVITE_TEAM.TITLE') }}
|
||||
</h2>
|
||||
<p
|
||||
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.INVITE_TEAM.BODY') }}
|
||||
</p>
|
||||
<div class="mt-10">
|
||||
<section class="space-y-5">
|
||||
<div class="flex gap-2">
|
||||
<form-input
|
||||
v-model="emailToAdd"
|
||||
name="email"
|
||||
spacing="compact"
|
||||
class="flex-grow"
|
||||
:placeholder="$t('START_ONBOARDING.INVITE_TEAM.PLACEHOLDER')"
|
||||
@keyup.enter="pushEmail"
|
||||
/>
|
||||
<woot-button
|
||||
variant="smooth"
|
||||
color-scheme="secondary"
|
||||
@click="pushEmail"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.INVITE_TEAM.ADD') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
<div
|
||||
class="rounded-md divide-y divide-slate-100 dark:divide-slate-700 border border-slate-200 dark:border-slate-700 max-h-[40vh] overflow-auto"
|
||||
:class="{
|
||||
' border-dashed grid place-content-center':
|
||||
emailsToInvite.length === 0,
|
||||
}"
|
||||
>
|
||||
<div v-if="emailsToInvite.length === 0" class="px-5 py-16">
|
||||
<p class="text-sm text-slate-400">
|
||||
{{ $t('START_ONBOARDING.INVITE_TEAM.LIST_EMPTY') }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-for="email in emailsToInvite"
|
||||
:key="email"
|
||||
class="flex items-center justify-between p-4"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<woot-thumbnail :username="email" size="24px" />
|
||||
<span
|
||||
class="text-slate-700 dark:text-slate-200 text-sm font-medium"
|
||||
>
|
||||
{{ email }}
|
||||
</span>
|
||||
</div>
|
||||
<woot-button
|
||||
variant="clear"
|
||||
size="small"
|
||||
color-scheme="secondary"
|
||||
icon="dismiss"
|
||||
@click="emailsToInvite.splice(emailsToInvite.indexOf(email), 1)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<submit-button
|
||||
button-class="flex justify-center w-full text-sm text-center"
|
||||
:button-text="$t('START_ONBOARDING.INVITE_TEAM.SUBMIT.BUTTON')"
|
||||
@click="onSubmit"
|
||||
/>
|
||||
|
||||
<woot-button
|
||||
variant="clear"
|
||||
color-scheme="secondary"
|
||||
size="expanded"
|
||||
is-expanded
|
||||
@click="skipToFoundersNote"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.INVITE_TEAM.SKIP') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormInput from 'v3/components/Form/Input.vue';
|
||||
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
|
||||
import AgentAPI from 'dashboard/api/agents.js';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SubmitButton,
|
||||
FormInput,
|
||||
},
|
||||
mixins: [alertMixin],
|
||||
data() {
|
||||
return {
|
||||
emailsToInvite: [],
|
||||
emailToAdd: '',
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
pushEmail() {
|
||||
if (!this.emailToAdd) return;
|
||||
this.emailsToInvite.push(this.emailToAdd);
|
||||
this.emailsToInvite = [...new Set(this.emailsToInvite)];
|
||||
this.emailToAdd = '';
|
||||
},
|
||||
async skipToFoundersNote() {
|
||||
try {
|
||||
await AgentAPI.bulkInvite({ emails: [] });
|
||||
this.$router.push({ name: 'onboarding_founders_note' });
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('START_ONBOARDING.INVITE_TEAM.SUBMIT.ERROR'));
|
||||
}
|
||||
this.$router.push({ name: 'onboarding_founders_note' });
|
||||
},
|
||||
async onSubmit() {
|
||||
if (this.emailsToInvite.length === 0) {
|
||||
this.showAlert(
|
||||
this.$t('START_ONBOARDING.INVITE_TEAM.SUBMIT.NO_MEMBER_ERROR')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await AgentAPI.bulkInvite({ emails: this.emailsToInvite });
|
||||
this.showAlert(this.$t('START_ONBOARDING.INVITE_TEAM.SUBMIT.SUCCESS'));
|
||||
this.$router.push({ name: 'onboarding_founders_note' });
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('START_ONBOARDING.INVITE_TEAM.SUBMIT.ERROR'));
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<section
|
||||
class="relative min-h-screen px-8 dark:text-white bg-[#FCFCFD] dark:bg-slate-900 flex items-center justify-center bg-[url('/assets/images/dashboard/onboarding/intro.svg')] dark:bg-[url('/assets/images/dashboard/onboarding/intro-dark.svg')] bg-[length:715px_555px] bg-no-repeat bg-[left_calc(-56px)_bottom_calc(-133px)]"
|
||||
>
|
||||
<div
|
||||
class="relative max-w-[1440px] w-full mx-auto flex justify-center gap-16"
|
||||
>
|
||||
<div class="relative w-4/12 px-16 py-[88px]">
|
||||
<div class="mb-10 z-0">
|
||||
<img
|
||||
:src="globalConfig.logo"
|
||||
:alt="globalConfig.installationName"
|
||||
class="h-8 w-auto block dark:hidden"
|
||||
/>
|
||||
<img
|
||||
v-if="globalConfig.logoDark"
|
||||
:src="globalConfig.logoDark"
|
||||
:alt="globalConfig.installationName"
|
||||
class="h-8 w-auto hidden dark:block"
|
||||
/>
|
||||
<h1
|
||||
class="font-bold dark:text-white text-slate-900 text-[40px] tracking-[1%] leading-[56px] mt-6 mb-16"
|
||||
>
|
||||
<slot name="intro">
|
||||
{{ intro }}
|
||||
</slot>
|
||||
</h1>
|
||||
</div>
|
||||
<div id="steps" class="z-0">
|
||||
<onboarding-step
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.name"
|
||||
v-bind="step"
|
||||
:step-number="index + 1"
|
||||
:icon="step.icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="relative w-8/12 py-[88px] flex items-center justify-center overflow-hidden px-24"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 h-full w-full bg-white dark:bg-slate-900 bg-[radial-gradient(var(--w-100)_1px,transparent_1px)] [background-size:16px_16px]"
|
||||
/>
|
||||
<div
|
||||
class="absolute h-full w-full overlay-gradient top-0 left-0 scale-y-110 blur-[3px]"
|
||||
/>
|
||||
<div
|
||||
id="modal-body"
|
||||
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative max-w-[560px]"
|
||||
>
|
||||
<h2
|
||||
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
|
||||
>
|
||||
<slot name="title">
|
||||
{{ title }}
|
||||
</slot>
|
||||
</h2>
|
||||
<slot name="body">
|
||||
<p
|
||||
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
|
||||
>
|
||||
{{ body }}
|
||||
</p>
|
||||
</slot>
|
||||
<div class="mt-10">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
import OnboardingStep from './OnboardingStep.vue';
|
||||
|
||||
const UISteps = [
|
||||
{
|
||||
name: 'profile',
|
||||
title: 'Setup Profile',
|
||||
icon: 'person',
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
name: 'company',
|
||||
title: 'Setup Company',
|
||||
icon: 'toolbox',
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
name: 'invite',
|
||||
title: 'Invite your team',
|
||||
icon: 'people-team',
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
name: 'founders-note',
|
||||
title: "Founder's Note",
|
||||
icon: '',
|
||||
hidden: true,
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
];
|
||||
export default {
|
||||
components: {
|
||||
OnboardingStep,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
props: {
|
||||
intro: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
body: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
currentStep: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
steps() {
|
||||
let foundCurrentStep = false;
|
||||
return UISteps.map(step => {
|
||||
if (step.name === this.currentStep) {
|
||||
foundCurrentStep = true;
|
||||
}
|
||||
return {
|
||||
...step,
|
||||
isActive: step.name === this.currentStep,
|
||||
isComplete: !foundCurrentStep,
|
||||
};
|
||||
});
|
||||
},
|
||||
...mapGetters({ globalConfig: 'globalConfig/get' }),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.overlay-gradient {
|
||||
background: linear-gradient(90deg, rgba(252, 252, 253, 0) 81.8%, #fcfcfd 100%),
|
||||
linear-gradient(270deg, rgba(252, 252, 253, 0) 76.93%, #fcfcfd 100%),
|
||||
linear-gradient(0deg, rgba(252, 252, 253, 0) 68.63%, #fcfcfd 100%),
|
||||
linear-gradient(180deg, rgba(252, 252, 253, 0) 73.2%, #fcfcfd 100%);
|
||||
}
|
||||
.dark .overlay-gradient {
|
||||
background: linear-gradient(270deg, rgba(24, 24, 26, 0) 76.93%, #151718 100%),
|
||||
linear-gradient(90deg, rgba(24, 24, 26, 0) 81.8%, #151718 100%),
|
||||
linear-gradient(0deg, rgba(24, 24, 26, 0) 68.63%, #151718 100%),
|
||||
linear-gradient(180deg, rgba(24, 24, 26, 0) 73.2%, #151718 100%);
|
||||
}
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center gap-2 p-2 text-lg font-bold mb-6 relative before:absolute before:h-10 before:w-[1px] before:bg-slate-200 before:-bottom-8 before:left-[24px] hide-before-of-last"
|
||||
class="flex items-center gap-2 p-2 text-lg font-medium mb-6 relative before:absolute before:h-10 before:w-[1px] before:bg-slate-200 before:-bottom-8 before:left-[24px] hide-before-of-last"
|
||||
:class="{
|
||||
'text-woot-500 ': isActive,
|
||||
'text-slate-400': !isActive || isComplete,
|
||||
@@ -10,7 +10,7 @@
|
||||
<div
|
||||
class="grid w-8 h-8 border border-solid rounded-full place-content-center"
|
||||
:class="{
|
||||
'border-woot-500': !isActive || isComplete,
|
||||
'border-woot-500': (!isActive && isComplete) || isComplete,
|
||||
'border-slate-200 dark:border-slate-600': !isActive && !isComplete,
|
||||
'text-woot-500': isComplete,
|
||||
}"
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<section
|
||||
class="relative min-h-screen px-8 dark:text-white bg-[#FCFCFD] dark:bg-slate-900 flex items-center justify-center bg-[url('/assets/images/dashboard/onboarding/intro.svg')] dark:bg-[url('/assets/images/dashboard/onboarding/intro-dark.svg')] bg-[length:715px_555px] bg-no-repeat bg-[left_calc(-56px)_bottom_calc(-133px)]"
|
||||
>
|
||||
<spinner v-if="!showIntroHeader" class="absolute inset-0" />
|
||||
<div
|
||||
v-else
|
||||
class="relative max-w-[1440px] w-full mx-auto flex gap-16 min-h-[80vh] justify-center"
|
||||
>
|
||||
<div class="relative w-5/12 px-16 py-[88px] mt-10">
|
||||
<div class="mb-10 z-0">
|
||||
<img
|
||||
:src="globalConfig.logo"
|
||||
:alt="globalConfig.installationName"
|
||||
class="h-8 w-auto block dark:hidden"
|
||||
/>
|
||||
<img
|
||||
v-if="globalConfig.logoDark"
|
||||
:src="globalConfig.logoDark"
|
||||
:alt="globalConfig.installationName"
|
||||
class="h-8 w-auto hidden dark:block"
|
||||
/>
|
||||
<transition name="fade">
|
||||
<h1
|
||||
v-show="showIntroHeader"
|
||||
class="font-bold dark:text-white text-slate-900 text-[40px] leading-[56px] whitespace-pre-line"
|
||||
>
|
||||
{{ intro }}
|
||||
</h1>
|
||||
</transition>
|
||||
</div>
|
||||
<transition name="slide-fade">
|
||||
<div v-if="showIntroHeader" id="steps" class="z-0">
|
||||
<onboarding-step
|
||||
v-for="(step, index) in steps"
|
||||
:key="step.name"
|
||||
v-bind="step"
|
||||
:step-number="index + 1"
|
||||
:icon="step.icon"
|
||||
/>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
<div
|
||||
class="relative w-7/12 py-[88px] flex justify-center overflow-hidden h-fit"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 h-full w-full bg-[#FCFCFD] dark:bg-slate-900 bg-[radial-gradient(var(--w-200)_1px,transparent_1px)] [background-size:16px_16px]"
|
||||
/>
|
||||
<div
|
||||
class="absolute h-full w-full overlay-gradient top-0 left-0 scale-y-110 blur-[3px]"
|
||||
/>
|
||||
|
||||
<transition name="slide-fade">
|
||||
<router-view />
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import globalConfigMixin from 'shared/mixins/globalConfigMixin';
|
||||
|
||||
import Spinner from 'shared/components/Spinner.vue';
|
||||
import OnboardingStep from './OnboardingStep.vue';
|
||||
import { UISteps, API_ONBOARDING_STEP_ROUTE } from './constants';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Spinner,
|
||||
OnboardingStep,
|
||||
},
|
||||
mixins: [globalConfigMixin],
|
||||
data() {
|
||||
return {
|
||||
showIntroHeader: false,
|
||||
showLoading: true,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
globalConfig: 'globalConfig/get',
|
||||
currentUser: 'getCurrentUser',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
getAccount: 'accounts/getAccount',
|
||||
}),
|
||||
currentStep() {
|
||||
return this.$route.name;
|
||||
},
|
||||
intro() {
|
||||
if (
|
||||
this.currentStep === 'onboarding_setup_company' ||
|
||||
this.currentStep === 'onboarding_invite_team'
|
||||
) {
|
||||
return this.$t('START_ONBOARDING.COMPANY.PAGE_TITLE', {
|
||||
userName: this.userName,
|
||||
});
|
||||
}
|
||||
|
||||
return this.$t('START_ONBOARDING.PROFILE.INTRO');
|
||||
},
|
||||
steps() {
|
||||
let foundCurrentStep = false;
|
||||
return UISteps.map(step => {
|
||||
if (step.name === this.currentStep) {
|
||||
foundCurrentStep = true;
|
||||
}
|
||||
return {
|
||||
...step,
|
||||
isActive: step.name === this.currentStep,
|
||||
isComplete: !foundCurrentStep,
|
||||
};
|
||||
});
|
||||
},
|
||||
userName() {
|
||||
return this.currentUser.name;
|
||||
},
|
||||
accountDetails() {
|
||||
return this.getAccount(this.currentAccountId);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
watch: {
|
||||
$route() {
|
||||
this.showIntroHeader = false;
|
||||
this.showLoading = true;
|
||||
|
||||
setTimeout(() => {
|
||||
this.showIntroHeader = true;
|
||||
}, 10);
|
||||
},
|
||||
accountDetails() {
|
||||
this.navigateToSavedStep();
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.showIntroHeader = true;
|
||||
this.showLoading = true;
|
||||
this.navigateToSavedStep();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.showIntroHeader = false;
|
||||
},
|
||||
methods: {
|
||||
navigateToSavedStep() {
|
||||
if (!this.accountDetails) return;
|
||||
const { custom_attributes: { onboarding_step: savedStep } = {} } =
|
||||
this.accountDetails;
|
||||
|
||||
const stepName = API_ONBOARDING_STEP_ROUTE[savedStep];
|
||||
if (stepName !== this.currentStep) {
|
||||
this.showLoading = false;
|
||||
this.$router.replace({ name: stepName });
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.overlay-gradient {
|
||||
background: linear-gradient(90deg, rgba(252, 252, 253, 0) 81.8%, #fcfcfd 95%),
|
||||
linear-gradient(270deg, rgba(252, 252, 253, 0) 76.93%, #fcfcfd 95%),
|
||||
linear-gradient(0deg, rgba(252, 252, 253, 0) 68.63%, #fcfcfd 95%),
|
||||
linear-gradient(180deg, rgba(252, 252, 253, 0) 73.2%, #fcfcfd 95%);
|
||||
}
|
||||
.dark .overlay-gradient {
|
||||
background: linear-gradient(270deg, rgba(24, 24, 26, 0) 76.93%, #151718 95%),
|
||||
linear-gradient(90deg, rgba(24, 24, 26, 0) 81.8%, #151718 95%),
|
||||
linear-gradient(0deg, rgba(24, 24, 26, 0) 68.63%, #151718 95%),
|
||||
linear-gradient(180deg, rgba(24, 24, 26, 0) 73.2%, #151718 95%);
|
||||
}
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.5s;
|
||||
}
|
||||
.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ {
|
||||
opacity: 0;
|
||||
}
|
||||
/* Enter and leave animations can use different */
|
||||
/* durations and timing functions. */
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.5s ease;
|
||||
}
|
||||
.slide-fade-leave-active {
|
||||
position: absolute;
|
||||
transition: all 0.4s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
.slide-fade-enter, .slide-fade-leave-to
|
||||
/* .slide-fade-leave-active below version 2.1.8 */ {
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,255 @@
|
||||
<template>
|
||||
<div
|
||||
id="modal-body"
|
||||
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative w-full max-w-[560px]"
|
||||
>
|
||||
<h2
|
||||
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.COMPANY.TITLE') }}
|
||||
</h2>
|
||||
<p
|
||||
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.COMPANY.BODY') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-10 space-y-8">
|
||||
<div class="space-y-3">
|
||||
<form-input
|
||||
v-model="companyName"
|
||||
icon="building-bank"
|
||||
name="companyName"
|
||||
spacing="compact"
|
||||
:has-error="$v.companyName.$error"
|
||||
:label="$t('START_ONBOARDING.COMPANY.COMPANY_NAME.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.COMPANY.COMPANY_NAME.PLACEHOLDER')"
|
||||
:error-message="$t('START_ONBOARDING.COMPANY.COMPANY_NAME.ERROR')"
|
||||
/>
|
||||
<form-select
|
||||
v-model="locale"
|
||||
name="locale"
|
||||
icon="local-language"
|
||||
:label="$t('START_ONBOARDING.COMPANY.LOCALE.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.COMPANY.LOCALE.PLACEHOLDER')"
|
||||
>
|
||||
<option
|
||||
v-for="lang in languagesSortedByCode"
|
||||
:key="lang.iso_639_1_code"
|
||||
:value="lang.iso_639_1_code"
|
||||
:selected="locale === lang.iso_639_1_code"
|
||||
>
|
||||
{{ lang.name }}
|
||||
</option>
|
||||
</form-select>
|
||||
<form-select
|
||||
v-model="timezone"
|
||||
name="timezone"
|
||||
icon="globe-clock"
|
||||
spacing="compact"
|
||||
:label="$t('START_ONBOARDING.COMPANY.TIMEZONE.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.COMPANY.TIMEZONE.PLACEHOLDER')"
|
||||
>
|
||||
<option
|
||||
v-for="zone in timeZones"
|
||||
:key="zone.label"
|
||||
:value="zone.value"
|
||||
:selected="timezone === zone.value"
|
||||
>
|
||||
{{ zone.label }}
|
||||
</option>
|
||||
</form-select>
|
||||
<form-radio-tags
|
||||
v-model="industry"
|
||||
name="industry"
|
||||
:label="$t('START_ONBOARDING.COMPANY.INDUSTRY.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.COMPANY.SIZE.PLACEHOLDER')"
|
||||
:options="industryOptions"
|
||||
/>
|
||||
<form-radio-tags
|
||||
v-model="companySize"
|
||||
name="companySize"
|
||||
:label="$t('START_ONBOARDING.COMPANY.SIZE.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.COMPANY.SIZE.PLACEHOLDER')"
|
||||
:options="companySizeOptions"
|
||||
/>
|
||||
</div>
|
||||
<submit-button
|
||||
button-class="flex justify-center w-full text-sm text-center"
|
||||
:button-text="$t('START_ONBOARDING.COMPANY.SUBMIT.BUTTON')"
|
||||
@click="onSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import FormInput from 'v3/components/Form/Input.vue';
|
||||
import FormSelect from 'v3/components/Form/Select.vue';
|
||||
import FormRadioTags from 'v3/components/Form/RadioTags.vue';
|
||||
import { required, minLength } from 'vuelidate/lib/validators';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import { ONBOARDING_STEP_NAMES } from 'dashboard/constants/globals';
|
||||
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
import { timeZoneOptions } from 'dashboard/routes/dashboard/settings/inbox/helpers/businessHour.js';
|
||||
import { getBrowserTimezone, getBrowserLocale } from 'v3/helpers/BrowserHelper';
|
||||
import {
|
||||
findMatchingOption,
|
||||
findIndustryOptions,
|
||||
} from 'v3/helpers/OnboardingHelper';
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import configMixin from 'shared/mixins/configMixin';
|
||||
export default {
|
||||
components: {
|
||||
FormInput,
|
||||
SubmitButton,
|
||||
FormSelect,
|
||||
FormRadioTags,
|
||||
},
|
||||
mixins: [configMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
companyName: '',
|
||||
timezone: '',
|
||||
locale: '',
|
||||
companySize: '',
|
||||
industry: '',
|
||||
companySizeOptions: [
|
||||
{ value: '1-10', label: '1-10' },
|
||||
{ value: '11-50', label: '11-50' },
|
||||
{ value: '51-250', label: '51-250' },
|
||||
{ value: '251-1000', label: '251-1000' },
|
||||
{ value: '1001+', label: 'Over 1000' },
|
||||
],
|
||||
industryOptions: [
|
||||
{ value: 'Information Technology', label: 'Information technology' },
|
||||
{ value: 'Finance', label: 'Finance' },
|
||||
{ value: 'Health & Medicine', label: 'Health & Medicine' },
|
||||
{ value: 'Education', label: 'Education' },
|
||||
{ value: 'E Commerce', label: 'E Commerce' },
|
||||
{ value: 'Other', label: 'Other' },
|
||||
],
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
companyName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentAccountId: 'getCurrentAccountId',
|
||||
}),
|
||||
accountDetails() {
|
||||
return this.getAccount(this.currentAccountId);
|
||||
},
|
||||
languagesSortedByCode() {
|
||||
const enabledLanguages = [...this.enabledLanguages];
|
||||
return enabledLanguages.sort((l1, l2) =>
|
||||
l1.iso_639_1_code.localeCompare(l2.iso_639_1_code)
|
||||
);
|
||||
},
|
||||
timeZones() {
|
||||
return [...timeZoneOptions()];
|
||||
},
|
||||
hasIntelligentData() {
|
||||
const {
|
||||
custom_attributes: { onboarding_step: onboardingStep },
|
||||
} = this.accountDetails;
|
||||
|
||||
return !onboardingStep !== ONBOARDING_STEP_NAMES.INVITE;
|
||||
},
|
||||
accountAttributes() {
|
||||
const attributes = this.accountDetails.custom_attributes;
|
||||
return {
|
||||
companyName: this.accountDetails.name,
|
||||
locale: this.accountDetails.locale,
|
||||
companySize: attributes.company_size,
|
||||
industry: attributes.industry,
|
||||
timezone: attributes.timezone,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
locale(val) {
|
||||
this.setLocale(val);
|
||||
},
|
||||
accountDetails() {
|
||||
this.initFormData();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.initFormData();
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit() {
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { industry } = this.accountAttributes;
|
||||
const industryValue =
|
||||
this.industry === 'Other' ? industry : this.industry;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('accounts/update', {
|
||||
name: this.companyName,
|
||||
timezone: this.timezone,
|
||||
locale: this.locale,
|
||||
company_size: this.companySize,
|
||||
industry: industryValue,
|
||||
onboarding_step: 'account_update',
|
||||
});
|
||||
|
||||
this.showAlert(this.$t('START_ONBOARDING.COMPANY.SUBMIT.SUCCESS'));
|
||||
this.$router.push({ name: 'onboarding_invite_team' });
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('START_ONBOARDING.COMPANY.SUBMIT.ERROR'));
|
||||
}
|
||||
},
|
||||
setLocale(locale) {
|
||||
this.$root.$i18n.locale = locale;
|
||||
},
|
||||
setLocaleFromBrowser() {
|
||||
const locale = getBrowserLocale(this.enabledLanguages);
|
||||
|
||||
if (locale) {
|
||||
this.locale = locale;
|
||||
this.setLocale(locale);
|
||||
}
|
||||
},
|
||||
setFromIntelligentData() {
|
||||
if (!this.accountDetails) return;
|
||||
|
||||
const { companyName, industry, companySize, timezone, locale } =
|
||||
this.accountAttributes;
|
||||
|
||||
this.companyName = companyName;
|
||||
this.industry = findIndustryOptions(industry);
|
||||
this.companySize = companySize;
|
||||
this.timezone = findMatchingOption(
|
||||
timezone,
|
||||
this.timeZones.map(zone => zone.value),
|
||||
getBrowserTimezone()
|
||||
);
|
||||
this.locale = locale || this.setLocaleFromBrowser();
|
||||
|
||||
this.setLocale(locale);
|
||||
},
|
||||
|
||||
initFormData() {
|
||||
if (this.hasIntelligentData) {
|
||||
this.setFromIntelligentData();
|
||||
} else {
|
||||
this.setSavedData();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,280 @@
|
||||
<template>
|
||||
<div
|
||||
id="modal-body"
|
||||
class="dark:shadow-[#000] rounded-3xl p-10 pt-14 border shadow border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 z-10 relative max-w-[560px]"
|
||||
>
|
||||
<h2
|
||||
class="font-bold text-[28px] leading-8 tracking-[2%] text-slate-900 dark:text-white"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.PROFILE.TITLE') }}
|
||||
</h2>
|
||||
<p
|
||||
class="text-base text-slate-600 leading-6 tracking-[-1.1%] mt-2 dark:text-slate-200"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.PROFILE.BODY') }}
|
||||
</p>
|
||||
<div class="mt-10">
|
||||
<form class="space-y-6" @submit="onSubmit">
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<with-label name="user_avatar">
|
||||
<template #label>
|
||||
{{ $t('START_ONBOARDING.PROFILE.AVATAR.LABEL') }}
|
||||
</template>
|
||||
<div class="flex gap-2">
|
||||
<woot-thumbnail
|
||||
v-if="avatarUrl"
|
||||
size="40px"
|
||||
:src="avatarUrl"
|
||||
:username="fullName"
|
||||
variant="square"
|
||||
:has-border="true"
|
||||
/>
|
||||
<div
|
||||
v-else
|
||||
class="rounded-md h-10 w-10 bg-slate-200 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g id="user-3-fill">
|
||||
<path
|
||||
id="Vector"
|
||||
d="M18.8574 20.5714H5.14307V18.8571C5.14307 17.7205 5.5946 16.6304 6.39832 15.8267C7.20205 15.0229 8.29214 14.5714 9.42878 14.5714H14.5716C15.7083 14.5714 16.7984 15.0229 17.6021 15.8267C18.4058 16.6304 18.8574 17.7205 18.8574 18.8571V20.5714ZM12.0002 12.8571C11.3248 12.8571 10.6561 12.7241 10.0321 12.4656C9.40816 12.2072 8.84122 11.8284 8.36366 11.3508C7.8861 10.8733 7.50728 10.3063 7.24883 9.68235C6.99038 9.05839 6.85735 8.38964 6.85735 7.71427C6.85735 7.0389 6.99038 6.37014 7.24883 5.74618C7.50728 5.12222 7.8861 4.55528 8.36366 4.07772C8.84122 3.60016 9.40816 3.22134 10.0321 2.96289C10.6561 2.70444 11.3248 2.57141 12.0002 2.57141C13.3642 2.57141 14.6723 3.11325 15.6368 4.07772C16.6012 5.04219 17.1431 6.3503 17.1431 7.71427C17.1431 9.07824 16.6012 10.3863 15.6368 11.3508C14.6723 12.3153 13.3642 12.8571 12.0002 12.8571Z"
|
||||
fill="#B9BBC6"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<input
|
||||
id="user_avatar"
|
||||
ref="imageUpload"
|
||||
name="user_avatar"
|
||||
class="invisible h-0 w-0"
|
||||
type="file"
|
||||
accept="image/png, image/jpeg, image/jpg, image/gif, image/webp"
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
<woot-button
|
||||
v-if="!avatarFile"
|
||||
color-scheme="secondary"
|
||||
variant="hollow"
|
||||
@click="onFileBrowse"
|
||||
>
|
||||
{{ $t('START_ONBOARDING.PROFILE.AVATAR.PLACEHOLDER') }}
|
||||
</woot-button>
|
||||
<woot-button
|
||||
v-else
|
||||
type="button"
|
||||
color-scheme="alert"
|
||||
variant="hollow"
|
||||
@click="deleteAvatar"
|
||||
>
|
||||
{{ $t('PROFILE_SETTINGS.DELETE_AVATAR') }}
|
||||
</woot-button>
|
||||
</div>
|
||||
</with-label>
|
||||
</div>
|
||||
<form-input
|
||||
v-model="fullName"
|
||||
name="full_name"
|
||||
spacing="compact"
|
||||
:has-error="$v.fullName.$error"
|
||||
:label="$t('START_ONBOARDING.PROFILE.FULL_NAME.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.PROFILE.FULL_NAME.PLACEHOLDER')"
|
||||
:error-message="$t('START_ONBOARDING.PROFILE.FULL_NAME.ERROR')"
|
||||
/>
|
||||
<form-input
|
||||
v-model="displayName"
|
||||
name="display_name"
|
||||
spacing="compact"
|
||||
:has-error="$v.displayName.$error"
|
||||
:label="$t('START_ONBOARDING.PROFILE.DISPLAY_NAME.LABEL')"
|
||||
:placeholder="
|
||||
$t('START_ONBOARDING.PROFILE.DISPLAY_NAME.PLACEHOLDER')
|
||||
"
|
||||
:error-message="$t('START_ONBOARDING.PROFILE.DISPLAY_NAME.ERROR')"
|
||||
/>
|
||||
<form-input
|
||||
v-model="phoneNumber"
|
||||
name="phone_number"
|
||||
spacing="compact"
|
||||
:has-error="$v.phoneNumber.$error"
|
||||
:label="$t('START_ONBOARDING.PROFILE.PHONE_NUMBER.LABEL')"
|
||||
:placeholder="
|
||||
$t('START_ONBOARDING.PROFILE.PHONE_NUMBER.PLACEHOLDER')
|
||||
"
|
||||
:error-message="$t('CONTACT_FORM.FORM.PHONE_NUMBER.ERROR')"
|
||||
@blur="$v.phoneNumber.$touch"
|
||||
/>
|
||||
<form-text-area
|
||||
v-model="signature"
|
||||
name="signature"
|
||||
spacing="compact"
|
||||
:allow-resize="false"
|
||||
:label="$t('START_ONBOARDING.PROFILE.SIGNATURE.LABEL')"
|
||||
:placeholder="$t('START_ONBOARDING.PROFILE.SIGNATURE.PLACEHOLDER')"
|
||||
/>
|
||||
</div>
|
||||
<submit-button
|
||||
button-class="flex justify-center w-full text-sm text-center"
|
||||
:button-text="$t('START_ONBOARDING.PROFILE.SUBMIT.BUTTON')"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import md5 from 'md5';
|
||||
import { required, minLength, helpers } from 'vuelidate/lib/validators';
|
||||
import parsePhoneNumber from 'libphonenumber-js';
|
||||
import { checkFileSizeLimit } from 'shared/helpers/FileHelper';
|
||||
import { validationMixin } from 'vuelidate';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import alertMixin from 'shared/mixins/alertMixin';
|
||||
import WithLabel from 'v3/components/Form/WithLabel.vue';
|
||||
import FormInput from 'v3/components/Form/Input.vue';
|
||||
import FormTextArea from 'v3/components/Form/Textarea.vue';
|
||||
import SubmitButton from 'dashboard/components/buttons/FormSubmitButton.vue';
|
||||
|
||||
const MAXIMUM_FILE_UPLOAD_SIZE = 4; // in MB
|
||||
|
||||
export default {
|
||||
components: {
|
||||
WithLabel,
|
||||
FormInput,
|
||||
SubmitButton,
|
||||
FormTextArea,
|
||||
},
|
||||
mixins: [validationMixin, alertMixin],
|
||||
data() {
|
||||
return {
|
||||
avatarUrl: '',
|
||||
avatarFile: '',
|
||||
fullName: '',
|
||||
displayName: '',
|
||||
phoneNumber: '',
|
||||
signature: '',
|
||||
};
|
||||
},
|
||||
validations: {
|
||||
fullName: {
|
||||
required,
|
||||
minLength: minLength(2),
|
||||
},
|
||||
displayName: {
|
||||
minLength: minLength(2),
|
||||
},
|
||||
phoneNumber: {
|
||||
validOrNoPhone: value =>
|
||||
!helpers.req(value) || parsePhoneNumber(value, 'US').isValid(),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getAccount: 'accounts/getAccount',
|
||||
currentUser: 'getCurrentUser',
|
||||
}),
|
||||
intelligentData() {
|
||||
const { clearbit_data: data } = this.getAccount;
|
||||
return data;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentUser(user = {}) {
|
||||
if (!user.name && this.intelligentData && this.intelligentData.name) {
|
||||
this.fullName = this.intelligentData.name;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const {
|
||||
email,
|
||||
name: fullName,
|
||||
avatar_url: avatarUrl,
|
||||
display_name: displayName,
|
||||
message_signature: signature,
|
||||
custom_attributes: { phone_number: phoneNumber } = {},
|
||||
} = this.currentUser || {};
|
||||
|
||||
if (!fullName && this.intelligentData && this.intelligentData.name) {
|
||||
this.fullName = this.intelligentData.name;
|
||||
} else {
|
||||
this.fullName = fullName;
|
||||
}
|
||||
|
||||
this.phoneNumber = phoneNumber;
|
||||
this.displayName = displayName;
|
||||
this.signature = signature;
|
||||
this.avatarUrl =
|
||||
avatarUrl ||
|
||||
`https://gravatar.com/avatar/${md5(email)}?s=400&d=robohash&r=x`;
|
||||
},
|
||||
|
||||
methods: {
|
||||
async onSubmit(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
this.$v.$touch();
|
||||
if (this.$v.$invalid) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await this.$store.dispatch('updateProfile', {
|
||||
name: this.fullName,
|
||||
displayName: this.displayName,
|
||||
phone_number: this.phoneNumber,
|
||||
avatar: this.avatarFile,
|
||||
message_signature: this.signature || '',
|
||||
});
|
||||
this.showAlert(this.$t('START_ONBOARDING.PROFILE.SUBMIT.SUCCESS'));
|
||||
|
||||
document.startViewTransition(() => {
|
||||
this.$router.push({ name: 'onboarding_setup_company' });
|
||||
});
|
||||
} catch (error) {
|
||||
this.showAlert(this.$t('START_ONBOARDING.PROFILE.SUBMIT.ERROR'));
|
||||
}
|
||||
},
|
||||
async deleteAvatar(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
const { email } = this.currentUser;
|
||||
this.avatarUrl = `https://gravatar.com/avatar/${md5(
|
||||
email
|
||||
)}?s=400&d=robohash&r=x`;
|
||||
this.avatarFile = '';
|
||||
this.$refs.imageUpload.value = '';
|
||||
},
|
||||
handleImageUpload() {
|
||||
const file = this.$refs.imageUpload.files[0];
|
||||
if (checkFileSizeLimit(file, MAXIMUM_FILE_UPLOAD_SIZE)) {
|
||||
this.avatarFile = file;
|
||||
this.avatarUrl = URL.createObjectURL(file);
|
||||
} else {
|
||||
this.showAlert(
|
||||
this.$t(
|
||||
'PROFILE_SETTINGS.FORM.MESSAGE_SIGNATURE_SECTION.IMAGE_UPLOAD_SIZE_ERROR',
|
||||
{
|
||||
size: MAXIMUM_FILE_UPLOAD_SIZE,
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.$refs.imageUpload.value = '';
|
||||
},
|
||||
onFileBrowse(event) {
|
||||
event.preventDefault();
|
||||
const fileInputElement = this.$refs.imageUpload;
|
||||
fileInputElement.click();
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
export const UISteps = [
|
||||
{
|
||||
name: 'onboarding_setup_profile',
|
||||
title: 'Setup Profile',
|
||||
icon: 'person',
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
name: 'onboarding_setup_company',
|
||||
title: 'Setup Company',
|
||||
icon: 'toolbox',
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
{
|
||||
name: 'onboarding_invite_team',
|
||||
title: 'Invite your team',
|
||||
icon: 'people-team',
|
||||
isActive: false,
|
||||
isComplete: false,
|
||||
},
|
||||
];
|
||||
|
||||
export const API_ONBOARDING_STEP_ROUTE = {
|
||||
profile_update: 'onboarding_setup_profile',
|
||||
account_update: 'onboarding_setup_company',
|
||||
invite_team: 'onboarding_invite_team',
|
||||
};
|
||||
@@ -30,7 +30,7 @@ module Avatarable
|
||||
|
||||
errors.add(:avatar, 'is too big') if avatar.byte_size > 15.megabytes
|
||||
|
||||
acceptable_types = ['image/jpeg', 'image/png', 'image/gif'].freeze
|
||||
acceptable_types = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze
|
||||
errors.add(:avatar, 'filetype not supported') unless acceptable_types.include?(avatar.content_type)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,6 +6,11 @@ if resource.custom_attributes.present?
|
||||
json.subscribed_quantity resource.custom_attributes['subscribed_quantity']
|
||||
json.subscription_status resource.custom_attributes['subscription_status']
|
||||
json.subscription_ends_on resource.custom_attributes['subscription_ends_on']
|
||||
json.industry resource.custom_attributes['industry'] if resource.custom_attributes['industry'].present?
|
||||
json.company_size resource.custom_attributes['company_size'] if resource.custom_attributes['company_size'].present?
|
||||
json.timezone resource.custom_attributes['timezone'] if resource.custom_attributes['timezone'].present?
|
||||
json.logo resource.custom_attributes['logo'] if resource.custom_attributes['logo'].present?
|
||||
json.onboarding_step resource.custom_attributes['onboarding_step'] if resource.custom_attributes['onboarding_step'].present?
|
||||
end
|
||||
end
|
||||
json.domain @account.domain
|
||||
|
||||
@@ -28,7 +28,8 @@ module Enterprise::Api::V2::AccountsController
|
||||
'industry' => data[:industry],
|
||||
'company_size' => data[:company_size],
|
||||
'timezone' => data[:timezone],
|
||||
'logo' => data[:logo]
|
||||
'logo' => data[:logo],
|
||||
'onboarding_step' => 'profile_update'
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
@@ -86,7 +86,23 @@ class Enterprise::ClearbitLookupService
|
||||
timezone: data.dig('company', 'timeZone'),
|
||||
logo: data.dig('company', 'logo'),
|
||||
industry: data.dig('company', 'category', 'industry'),
|
||||
company_size: data.dig('company', 'metrics', 'employees')
|
||||
company_size: normalize_company_size(data.dig('company', 'metrics', 'employeesRange'))
|
||||
}
|
||||
end
|
||||
|
||||
def self.normalize_company_size(size)
|
||||
sizemap = {
|
||||
'1-10' => '1-10',
|
||||
'11-50' => '11-50',
|
||||
'51-250' => '51-250',
|
||||
'251-1K' => '250-1000',
|
||||
'1K-5K' => '1001+',
|
||||
'5K-10K' => '1001+',
|
||||
'10K-50K' => '1001+',
|
||||
'50K-100K' => '1001+',
|
||||
'100K+' => '1001+'
|
||||
}
|
||||
|
||||
sizemap[size] || nil
|
||||
end
|
||||
end
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 55 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 55 KiB |
@@ -0,0 +1,5 @@
|
||||
<svg width="32" height="33" viewBox="0 0 32 33" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="waving_hand_FILL0_wght400_GRAD0_opsz24 1">
|
||||
<path id="Vector" d="M14.3322 15.8336L23.7655 6.40029C24.0322 6.13363 24.3433 6.00029 24.6988 6.00029C25.0544 6.00029 25.3655 6.13363 25.6322 6.40029C25.8988 6.66696 26.0322 6.97807 26.0322 7.33363C26.0322 7.68918 25.8988 8.00029 25.6322 8.26696L16.2322 17.7003L14.3322 15.8336ZM17.6322 19.1336L26.0988 10.6336C26.3655 10.367 26.6822 10.2336 27.0488 10.2336C27.4155 10.2336 27.7322 10.367 27.9988 10.6336C28.2655 10.9003 28.3988 11.217 28.3988 11.5836C28.3988 11.9503 28.2655 12.267 27.9988 12.5336L19.5322 21.0003L17.6322 19.1336ZM7.03216 25.467C5.00994 23.4447 3.99883 21.0114 3.99883 18.167C3.99883 15.3225 5.00994 12.8892 7.03216 10.867L11.0322 6.86696L12.9988 8.83363C13.1544 8.98918 13.2877 9.15029 13.3988 9.31696C13.5099 9.48363 13.621 9.65585 13.7322 9.83363L18.6655 4.86696C18.9322 4.60029 19.2488 4.46696 19.6155 4.46696C19.9822 4.46696 20.2988 4.60029 20.5655 4.86696C20.8322 5.13363 20.9655 5.45029 20.9655 5.81696C20.9655 6.18363 20.8322 6.50029 20.5655 6.76696L14.7988 12.5336L11.9655 15.3336L12.5988 15.967C13.621 16.9892 14.1099 18.2114 14.0655 19.6336C14.021 21.0558 13.4766 22.2892 12.4322 23.3336L10.5322 21.467C11.0433 20.9558 11.3266 20.3503 11.3822 19.6503C11.4377 18.9503 11.2099 18.3447 10.6988 17.8336L9.13216 16.3003C8.86549 16.0336 8.73216 15.717 8.73216 15.3503C8.73216 14.9836 8.86549 14.667 9.13216 14.4003L11.0322 12.5336C11.2988 12.267 11.4322 11.9503 11.4322 11.5836C11.4322 11.217 11.2988 10.9003 11.0322 10.6336L8.89883 12.767C7.38772 14.2781 6.63216 16.0836 6.63216 18.1836C6.63216 20.2836 7.38772 22.0892 8.89883 23.6003C10.4099 25.1114 12.2211 25.867 14.3322 25.867C16.4433 25.867 18.2544 25.1114 19.7655 23.6003L27.7322 15.6003C27.9988 15.3336 28.3155 15.2003 28.6822 15.2003C29.0488 15.2003 29.3655 15.3336 29.6322 15.6003C29.8988 15.867 30.0322 16.1836 30.0322 16.5503C30.0322 16.917 29.8988 17.2336 29.6322 17.5003L21.6322 25.467C19.6099 27.4892 17.1766 28.5003 14.3322 28.5003C11.4877 28.5003 9.05438 27.4892 7.03216 25.467ZM22.6655 31.2003V28.5003C24.1322 28.5003 25.3877 27.9781 26.4322 26.9336C27.4766 25.8892 27.9988 24.6336 27.9988 23.167H30.6988C30.6988 25.3892 29.9155 27.2836 28.3488 28.8503C26.7822 30.417 24.8877 31.2003 22.6655 31.2003ZM1.29883 9.83363C1.29883 7.6114 2.08216 5.71696 3.64883 4.15029C5.21549 2.58363 7.10994 1.80029 9.33216 1.80029V4.50029C7.86549 4.50029 6.60994 5.02252 5.56549 6.06696C4.52105 7.1114 3.99883 8.36696 3.99883 9.83363H1.29883Z" fill="#1F93FF"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -213,6 +213,16 @@ RSpec.describe 'Accounts API', type: :request do
|
||||
end
|
||||
end
|
||||
|
||||
it 'updates onboarding step to invite_agents if onboarding step is present in account custom attributes' do
|
||||
account.update(custom_attributes: { onboarding_step: 'completed' })
|
||||
put "/api/v1/accounts/#{account.id}",
|
||||
params: params,
|
||||
headers: admin.create_new_auth_token,
|
||||
as: :json
|
||||
|
||||
expect(account.reload.custom_attributes['onboarding_step']).to eq('invite_agents')
|
||||
end
|
||||
|
||||
it 'Throws error 422' do
|
||||
params[:name] = 'test' * 999
|
||||
|
||||
|
||||
@@ -41,5 +41,15 @@ RSpec.describe 'Agents API', type: :request do
|
||||
expect(response.body).to include('Account limit exceeded. Please purchase more licenses')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when onboarding step is present in account custom attributes' do
|
||||
it 'removes onboarding step from account custom attributes' do
|
||||
account.update(custom_attributes: { onboarding_step: 'completed' })
|
||||
|
||||
post "/api/v1/accounts/#{account.id}/agents/bulk_create", params: bulk_create_params, headers: admin.create_new_auth_token
|
||||
|
||||
expect(account.reload.custom_attributes).not_to include('onboarding_step')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -48,6 +48,7 @@ RSpec.describe Enterprise::Api::V2::AccountsController, type: :request do
|
||||
expect(custom_attributes['industry']).to eq('Software')
|
||||
expect(custom_attributes['company_size']).to eq('51-200')
|
||||
expect(custom_attributes['timezone']).to eq('America/Los_Angeles')
|
||||
expect(custom_attributes['onboarding_step']).to eq('profile_update')
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ RSpec.describe Enterprise::ClearbitLookupService do
|
||||
expect(result).to eq({
|
||||
:avatar => 'https://example.com/avatar.png',
|
||||
:company_name => 'Doe Inc.',
|
||||
:company_size => '1-10',
|
||||
:company_size => '1001+',
|
||||
:industry => 'Software',
|
||||
:logo => nil,
|
||||
:name => 'John Doe',
|
||||
|
||||
@@ -20,7 +20,7 @@ FactoryBot.define do
|
||||
'industry' => 'Software'
|
||||
},
|
||||
'metrics' => {
|
||||
'employees' => '1-10'
|
||||
'employeesRange' => '10K-50K'
|
||||
}
|
||||
}
|
||||
}.to_json
|
||||
|
||||
Reference in New Issue
Block a user