mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-24 02:52:24 -08:00
refactor: Add Onboarding call prompts (#3682)
* ✨ Implemented initial onboarding call prompt logic * ✨ Added onboarding call prompt feature environment variable * ✨ Implemented onboarding session signup modal * 📈 Added initial telemetry for the onboarding call prompt * ✔️ Fixing linter error in server.ts * 💄 Updating onboaring call prompt and modal wording and styling * ✨ Implemented initial version of fake doors feature * ✨ Added parameters to onboarding call prompt request * ✨ Finished implementing fake doors in settings * 🔨 Updating onboarding call prompt fetching logic (fetching before timeout starts) * 👌 Updating onboarding call prompt and fake door components based on the front-end review feedback * ✨ Updated fake doors so they support UI location specification. Added credentials UI fake doors. * ⚡ Added checkbox to the signup form, improved N8NCheckbox formatting to better handle overflow * 💄 Moving seignup checkbox label text to i18n file, updating checkbox component css to force text wrap * ✨ Update API calls to work with the new workflow request and response formats * 👌 Updating fake door front-end based on the review feedback * 👌 Updating onboarding call prompt and fake doors UI based in the product feedback * ✨ Updated onboarding call prompts front-end to work with new endpoints and added new telemetry events * 🐛 Fixing onboarding call prompts not appearing in first user sessions * ⚡️ add createdAt to PublicUser * 👌 Updating onboarding call prompts front-end to work with the latest back-end and addressing latest product review * ✨ Improving error handling when submitting user emails on signup * 💄 Updating info text on Logging feature page * 💄 Updating first onboarding call prompt timeout to 5 minutes * 💄 Fixing `N8nCheckbox` component font overflow Co-authored-by: Ben Hesseldieck <b.hesseldieck@gmail.com>
This commit is contained in:
parent
553b14a13c
commit
3ebfa45570
|
@ -909,4 +909,13 @@ export const schema = {
|
|||
default: 'en',
|
||||
env: 'N8N_DEFAULT_LOCALE',
|
||||
},
|
||||
|
||||
onboardingCallPrompt: {
|
||||
enabled: {
|
||||
doc: 'Whether onboarding call propmpt feature is available',
|
||||
format: Boolean,
|
||||
default: true,
|
||||
env: 'N8N_ONBOARDING_CALL_PROMPTS_ENABLED',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -512,6 +512,7 @@ export interface IN8nUISettings {
|
|||
enabled: boolean;
|
||||
host: string;
|
||||
};
|
||||
onboardingCallPromptEnabled: boolean;
|
||||
missingPackages?: boolean;
|
||||
executionMode: 'regular' | 'queue';
|
||||
communityNodesEnabled: boolean;
|
||||
|
|
|
@ -335,6 +335,7 @@ class App {
|
|||
enabled: config.getEnv('templates.enabled'),
|
||||
host: config.getEnv('templates.host'),
|
||||
},
|
||||
onboardingCallPromptEnabled: config.getEnv('onboardingCallPrompt.enabled'),
|
||||
executionMode: config.getEnv('executions.mode'),
|
||||
communityNodesEnabled: config.getEnv('nodes.communityPackages.enabled'),
|
||||
};
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* eslint-disable import/no-cycle */
|
||||
import { Application } from 'express';
|
||||
import { JwtFromRequestFunction } from 'passport-jwt';
|
||||
import type { IExternalHooksClass, IPersonalizationSurveyAnswers } from '../Interfaces';
|
||||
import { ActiveWorkflowRunner } from '..';
|
||||
import type { IExternalHooksClass, IPersonalizationSurveyAnswers } from '../Interfaces';
|
||||
|
||||
export interface JwtToken {
|
||||
token: string;
|
||||
|
@ -28,6 +28,7 @@ export interface PublicUser {
|
|||
personalizationAnswers?: IPersonalizationSurveyAnswers | null;
|
||||
password?: string;
|
||||
passwordResetToken?: string;
|
||||
createdAt: Date;
|
||||
isPending: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -121,7 +121,6 @@ export function sanitizeUser(user: User, withoutKeys?: string[]): PublicUser {
|
|||
password,
|
||||
resetPasswordToken,
|
||||
resetPasswordTokenExpiration,
|
||||
createdAt,
|
||||
updatedAt,
|
||||
apiKey,
|
||||
...sanitizedUser
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<template>
|
||||
<el-checkbox
|
||||
v-bind="$props"
|
||||
:class="$style.n8nCheckbox"
|
||||
:disabled="disabled"
|
||||
:indeterminate="indeterminate"
|
||||
:value="value"
|
||||
|
@ -63,4 +64,14 @@ export default Vue.extend({
|
|||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
.n8nCheckbox {
|
||||
display: flex !important;
|
||||
white-space: normal !important;
|
||||
|
||||
span {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -72,9 +72,9 @@ FormInputs.args = {
|
|||
name: 'agree',
|
||||
properties: {
|
||||
type: 'checkbox',
|
||||
label: 'Signup for newsletter Signup for newsletter Signup for newsletter vSignup for newsletter Signup for newsletter Signup for newsletter Signup for newsletter Signup for newsletter Signup for newsletter Signup for newsletter v vSignup for newsletter Signup for newsletter Signup for newsletter Signup for newsletter',
|
||||
label: 'Signup for newsletter and somebody from our marketing team will get in touch with you as soon as possible. You will not spam you, just want to send you some love every now and then ❤️',
|
||||
labelSize: 'small',
|
||||
tooltipText: 'Check this if you agree to be contacted by our marketing team Check this if you agree to be contacted by our marketing team Check this if you agree to be contacted by our marketing team Check this if you agree to be contacted by our marketing team'
|
||||
tooltipText: 'Check this if you agree to be contacted by our marketing team'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
@ -15,7 +15,7 @@ export type IFormInput = {
|
|||
initialValue?: string | number | boolean | null;
|
||||
properties: {
|
||||
label?: string;
|
||||
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info';
|
||||
type?: 'text' | 'email' | 'password' | 'select' | 'multi-select' | 'info'| 'checkbox';
|
||||
maxlength?: number;
|
||||
required?: boolean;
|
||||
showRequiredAsterisk?: boolean;
|
||||
|
|
|
@ -24,6 +24,7 @@ import {
|
|||
WorkflowExecuteMode,
|
||||
PublicInstalledPackage,
|
||||
} from 'n8n-workflow';
|
||||
import { FAKE_DOOR_FEATURES } from './constants';
|
||||
|
||||
export * from 'n8n-design-system/src/types';
|
||||
|
||||
|
@ -542,6 +543,7 @@ export interface IUserResponse {
|
|||
globalRole?: {
|
||||
name: IRole;
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
personalizationAnswers?: IPersonalizationSurveyAnswersV1 | IPersonalizationSurveyAnswersV2 | null;
|
||||
isPending: boolean;
|
||||
|
@ -552,6 +554,7 @@ export interface IUser extends IUserResponse {
|
|||
isPendingUser: boolean;
|
||||
isOwner: boolean;
|
||||
fullName?: string;
|
||||
createdAt?: Date;
|
||||
}
|
||||
|
||||
export interface IVersionNotificationSettings {
|
||||
|
@ -701,6 +704,7 @@ export interface IN8nUISettings {
|
|||
latestVersion: number;
|
||||
path: string;
|
||||
};
|
||||
onboardingCallPromptEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||
|
@ -917,6 +921,7 @@ export interface IUiState {
|
|||
mappingTelemetry: {[key: string]: string | number | boolean};
|
||||
};
|
||||
mainPanelPosition: number;
|
||||
fakeDoorFeatures: IFakeDoor[];
|
||||
draggable: {
|
||||
isDragging: boolean;
|
||||
type: string;
|
||||
|
@ -928,6 +933,19 @@ export interface IUiState {
|
|||
|
||||
export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose';
|
||||
|
||||
export type IFakeDoor = {
|
||||
id: FAKE_DOOR_FEATURES,
|
||||
featureName: string,
|
||||
icon?: string,
|
||||
infoText?: string,
|
||||
actionBoxTitle: string,
|
||||
actionBoxDescription: string,
|
||||
linkURL: string,
|
||||
uiLocations: IFakeDoorLocation[],
|
||||
};
|
||||
|
||||
export type IFakeDoorLocation = 'settings' | 'credentialsModal';
|
||||
|
||||
export interface ISettingsState {
|
||||
settings: IN8nUISettings;
|
||||
promptsData: IN8nPrompts;
|
||||
|
@ -938,6 +956,7 @@ export interface ISettingsState {
|
|||
latestVersion: number;
|
||||
path: string;
|
||||
};
|
||||
onboardingCallPromptEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface ITemplateState {
|
||||
|
@ -1006,6 +1025,16 @@ export interface IInviteResponse {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
export interface IOnboardingCallPromptResponse {
|
||||
nextPrompt: IOnboardingCallPrompt;
|
||||
}
|
||||
|
||||
export interface IOnboardingCallPrompt {
|
||||
title: string;
|
||||
body: string;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export interface ITab {
|
||||
value: string | number;
|
||||
label?: string;
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { IInviteResponse, IPersonalizationSurveyAnswersV2, IRestApiContext, IUserResponse } from '@/Interface';
|
||||
import { IInviteResponse, IOnboardingCallPromptResponse, IPersonalizationSurveyAnswersV2, IRestApiContext, IUserResponse } from '@/Interface';
|
||||
import { IDataObject } from 'n8n-workflow';
|
||||
import { makeRestApiRequest } from './helpers';
|
||||
import { get, makeRestApiRequest } from './helpers';
|
||||
|
||||
export function loginCurrentUser(context: IRestApiContext): Promise<IUserResponse | null> {
|
||||
return makeRestApiRequest(context, 'GET', '/login');
|
||||
|
|
49
packages/editor-ui/src/api/workflow-webhooks.ts
Normal file
49
packages/editor-ui/src/api/workflow-webhooks.ts
Normal file
|
@ -0,0 +1,49 @@
|
|||
import { IOnboardingCallPromptResponse, IUser } from "@/Interface";
|
||||
import { get, post } from "./helpers";
|
||||
|
||||
const N8N_API_BASE_URL = 'https://api.n8n.io/api';
|
||||
const ONBOARDING_PROMPTS_ENDPOINT = '/prompts/onboarding';
|
||||
const CONTACT_EMAIL_SUBMISSION_ENDPOINT = '/accounts/onboarding';
|
||||
|
||||
export async function fetchNextOnboardingPrompt(instanceId: string, currentUer: IUser): Promise<IOnboardingCallPromptResponse> {
|
||||
return await get(
|
||||
N8N_API_BASE_URL,
|
||||
ONBOARDING_PROMPTS_ENDPOINT,
|
||||
{
|
||||
instance_id: instanceId,
|
||||
user_id: `${instanceId}#${currentUer.id}`,
|
||||
is_owner: currentUer.isOwner,
|
||||
survey_results: currentUer.personalizationAnswers,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export async function applyForOnboardingCall(instanceId: string, currentUer: IUser, email: string): Promise<string> {
|
||||
try {
|
||||
const response = await post(
|
||||
N8N_API_BASE_URL,
|
||||
ONBOARDING_PROMPTS_ENDPOINT,
|
||||
{
|
||||
instance_id: instanceId,
|
||||
user_id: `${instanceId}#${currentUer.id}`,
|
||||
email,
|
||||
},
|
||||
);
|
||||
return response;
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
export async function submitEmailOnSignup(instanceId: string, currentUer: IUser, email: string, agree: boolean): Promise<string> {
|
||||
return await post(
|
||||
N8N_API_BASE_URL,
|
||||
CONTACT_EMAIL_SUBMISSION_ENDPOINT,
|
||||
{
|
||||
instance_id: instanceId,
|
||||
user_id: `${instanceId}#${currentUer.id}`,
|
||||
email,
|
||||
agree,
|
||||
},
|
||||
);
|
||||
}
|
|
@ -57,6 +57,14 @@
|
|||
<n8n-menu-item index="connection"
|
||||
><span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.connection') }}</span></n8n-menu-item
|
||||
>
|
||||
<n8n-menu-item
|
||||
v-for="fakeDoor in credentialsFakeDoorFeatures"
|
||||
v-bind:key="fakeDoor.featureName"
|
||||
:index="`coming-soon/${fakeDoor.id}`"
|
||||
:class="$style.tab"
|
||||
>
|
||||
<span slot="title">{{ $locale.baseText(fakeDoor.featureName) }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="details"
|
||||
><span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.details') }}</span></n8n-menu-item
|
||||
>
|
||||
|
@ -89,6 +97,9 @@
|
|||
@accessChange="onNodeAccessChange"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="activeTab.startsWith('coming-soon')" :class="$style.mainContent">
|
||||
<FeatureComingSoon :featureId="activeTab.split('/')[1]"></FeatureComingSoon>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@ -100,6 +111,7 @@ import Vue from 'vue';
|
|||
import {
|
||||
ICredentialsDecryptedResponse,
|
||||
ICredentialsResponse,
|
||||
IFakeDoor,
|
||||
} from '@/Interface';
|
||||
|
||||
import {
|
||||
|
@ -108,6 +120,7 @@ import {
|
|||
ICredentialNodeAccess,
|
||||
ICredentialsDecrypted,
|
||||
ICredentialType,
|
||||
INode,
|
||||
INodeCredentialTestResult,
|
||||
INodeParameters,
|
||||
INodeProperties,
|
||||
|
@ -126,6 +139,7 @@ import CredentialInfo from './CredentialInfo.vue';
|
|||
import SaveButton from '../SaveButton.vue';
|
||||
import Modal from '../Modal.vue';
|
||||
import InlineNameEdit from '../InlineNameEdit.vue';
|
||||
import FeatureComingSoon from '../FeatureComingSoon.vue';
|
||||
|
||||
interface NodeAccessMap {
|
||||
[nodeType: string]: ICredentialNodeAccess | null;
|
||||
|
@ -140,6 +154,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
InlineNameEdit,
|
||||
Modal,
|
||||
SaveButton,
|
||||
FeatureComingSoon,
|
||||
},
|
||||
props: {
|
||||
modalName: {
|
||||
|
@ -351,6 +366,9 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
}
|
||||
return true;
|
||||
},
|
||||
credentialsFakeDoorFeatures(): IFakeDoor[] {
|
||||
return this.$store.getters['ui/getFakeDoorByLocation']('credentialsModal');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async beforeClose() {
|
||||
|
@ -474,6 +492,15 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
},
|
||||
onTabSelect(tab: string) {
|
||||
this.activeTab = tab;
|
||||
const tabName: string = tab.replaceAll('coming-soon/', '');
|
||||
const credType: string = this.credentialType ? this.credentialType.name : '';
|
||||
const activeNode: INode | null = this.$store.getters.activeNode;
|
||||
|
||||
this.$telemetry.track('User viewed credential tab', {
|
||||
credential_type: credType,
|
||||
node_type: activeNode ? activeNode.type : null,
|
||||
tab: tabName,
|
||||
});
|
||||
},
|
||||
onNodeAccessChange({name, value}: {name: string, value: boolean}) {
|
||||
this.hasUnsavedChanges = true;
|
||||
|
|
71
packages/editor-ui/src/components/FeatureComingSoon.vue
Normal file
71
packages/editor-ui/src/components/FeatureComingSoon.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<div v-if="this.featureInfo" :class="$style.container">
|
||||
<div v-if="showHeading" :class="[$style.headingContainer, 'mb-l']">
|
||||
<n8n-heading size="2xlarge">{{ $locale.baseText(featureInfo.featureName) }}</n8n-heading>
|
||||
</div>
|
||||
<div v-if="featureInfo.infoText" class="mt-3xl mb-l">
|
||||
<n8n-info-tip theme="info" type="note">
|
||||
<template>
|
||||
<span v-html="$locale.baseText(featureInfo.infoText)"></span>
|
||||
</template>
|
||||
</n8n-info-tip>
|
||||
</div>
|
||||
<div :class="$style.actionBoxContainer">
|
||||
<n8n-action-box
|
||||
:heading="$locale.baseText(featureInfo.actionBoxTitle)"
|
||||
:description="$locale.baseText(featureInfo.actionBoxDescription)"
|
||||
:buttonText="$locale.baseText('fakeDoor.actionBox.button.label')"
|
||||
@click="openLinkPage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { FAKE_DOOR_FEATURES } from '@/constants';
|
||||
import { IFakeDoor } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FeatureComingSoon',
|
||||
props: {
|
||||
featureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
userId(): string {
|
||||
return this.$store.getters['users/currentUserId'];
|
||||
},
|
||||
versionCli(): string {
|
||||
return this.$store.getters['settings/versionCli'];
|
||||
},
|
||||
instanceId(): string {
|
||||
return this.$store.getters.instanceId;
|
||||
},
|
||||
featureInfo(): IFakeDoor {
|
||||
return this.$store.getters['ui/getFakeDoorById'](this.featureId);
|
||||
},
|
||||
showHeading(): boolean {
|
||||
const featuresWithoutHeading = [
|
||||
FAKE_DOOR_FEATURES.SHARING.toString(),
|
||||
];
|
||||
return !featuresWithoutHeading.includes(this.featureId);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openLinkPage() {
|
||||
window.open(`${this.featureInfo.linkURL}&u=${this.instanceId}#${this.userId}&v=${this.versionCli}`, '_blank');
|
||||
this.$telemetry.track('user clicked feature waiting list button', { feature: this.featureId });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.actionBoxContainer {
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -89,6 +89,10 @@
|
|||
<ActivationModal />
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="ONBOARDING_CALL_SIGNUP_MODAL_KEY">
|
||||
<OnboardingCallSignupModal />
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="COMMUNITY_PACKAGE_INSTALL_MODAL_KEY">
|
||||
<CommunityPackageInstallModal />
|
||||
</ModalRoot>
|
||||
|
@ -102,6 +106,7 @@
|
|||
/>
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -120,6 +125,7 @@ import {
|
|||
DUPLICATE_MODAL_KEY,
|
||||
EXECUTIONS_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
|
@ -140,6 +146,7 @@ import InviteUsersModal from "./InviteUsersModal.vue";
|
|||
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
||||
import DuplicateWorkflowDialog from "./DuplicateWorkflowDialog.vue";
|
||||
import ModalRoot from "./ModalRoot.vue";
|
||||
import OnboardingCallSignupModal from './OnboardingCallSignupModal.vue';
|
||||
import PersonalizationModal from "./PersonalizationModal.vue";
|
||||
import TagsManager from "./TagsManager/TagsManager.vue";
|
||||
import UpdatesPanel from "./UpdatesPanel.vue";
|
||||
|
@ -167,6 +174,7 @@ export default Vue.extend({
|
|||
InviteUsersModal,
|
||||
ExecutionsList,
|
||||
ModalRoot,
|
||||
OnboardingCallSignupModal,
|
||||
PersonalizationModal,
|
||||
TagsManager,
|
||||
UpdatesPanel,
|
||||
|
@ -185,6 +193,7 @@ export default Vue.extend({
|
|||
CHANGE_PASSWORD_MODAL_KEY,
|
||||
DELETE_USER_MODAL_KEY,
|
||||
DUPLICATE_MODAL_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
|
|
128
packages/editor-ui/src/components/OnboardingCallSignupModal.vue
Normal file
128
packages/editor-ui/src/components/OnboardingCallSignupModal.vue
Normal file
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="ONBOARDING_CALL_SIGNUP_MODAL_KEY"
|
||||
:title="$locale.baseText('onboardingCallSignupModal.title')"
|
||||
:eventBus="modalBus"
|
||||
:center="true"
|
||||
:showClose="false"
|
||||
:beforeClose="onModalClose"
|
||||
width="460px"
|
||||
>
|
||||
<template slot="content">
|
||||
<div class="pb-m">
|
||||
<n8n-text>
|
||||
{{ $locale.baseText('onboardingCallSignupModal.description') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div @keyup.enter="onSignup">
|
||||
<n8n-input v-model="email" :placeholder="$locale.baseText('onboardingCallSignupModal.emailInput.placeholder')" />
|
||||
<n8n-text v-if="showError" size="small" class="mt-4xs" tag="div" color="danger">
|
||||
{{ $locale.baseText('onboardingCallSignupModal.infoText.emailError') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</template>
|
||||
<template slot="footer">
|
||||
<div :class="$style.buttonsContainer">
|
||||
<n8n-button
|
||||
:label="$locale.baseText('onboardingCallSignupModal.cancelButton.label')"
|
||||
:disabled="loading"
|
||||
size="medium"
|
||||
float="right"
|
||||
type="outline"
|
||||
@click="onCancel"
|
||||
/>
|
||||
<n8n-button
|
||||
:disabled="email === '' || loading"
|
||||
:label="$locale.baseText('onboardingCallSignupModal.signupButton.label')"
|
||||
size="medium"
|
||||
float="right"
|
||||
:loading="loading"
|
||||
@click="onSignup"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
import {
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
VALID_EMAIL_REGEX,
|
||||
} from '@/constants';
|
||||
import Modal from './Modal.vue';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { showMessage } from './mixins/showMessage';
|
||||
|
||||
export default mixins(
|
||||
showMessage,
|
||||
).extend({
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
name: 'OnboardingCallSignupModal',
|
||||
props: [ 'modalName' ],
|
||||
data() {
|
||||
return {
|
||||
email: '',
|
||||
modalBus: new Vue(),
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
showError: false,
|
||||
okToClose: false,
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
isEmailValid(): boolean {
|
||||
return VALID_EMAIL_REGEX.test(String(this.email).toLowerCase());
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async onSignup() {
|
||||
if (!this.isEmailValid) {
|
||||
this.showError = true;
|
||||
return;
|
||||
}
|
||||
this.showError = false;
|
||||
this.loading = true;
|
||||
this.okToClose = false;
|
||||
|
||||
try {
|
||||
await this.$store.dispatch('ui/applyForOnboardingCall', { email: this.email });
|
||||
this.$showMessage({
|
||||
type: 'success',
|
||||
title: this.$locale.baseText('onboardingCallSignupSucess.title'),
|
||||
message: this.$locale.baseText('onboardingCallSignupSucess.message'),
|
||||
});
|
||||
this.okToClose = true;
|
||||
this.modalBus.$emit('close');
|
||||
} catch (e) {
|
||||
this.$showError(
|
||||
e,
|
||||
this.$locale.baseText('onboardingCallSignupFailed.title'),
|
||||
this.$locale.baseText('onboardingCallSignupFailed.message'),
|
||||
);
|
||||
this.loading = false;
|
||||
this.okToClose = true;
|
||||
}
|
||||
},
|
||||
async onCancel() {
|
||||
this.okToClose = true;
|
||||
this.modalBus.$emit('close');
|
||||
},
|
||||
onModalClose() {
|
||||
return this.okToClose;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.buttonsContainer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
column-gap: var(--spacing-xs);
|
||||
}
|
||||
</style>
|
|
@ -108,6 +108,9 @@ import {
|
|||
OTHER_FOCUS,
|
||||
COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
OTHER_COMPANY_INDUSTRY_EXTENDED_KEY,
|
||||
ONBOARDING_PROMPT_TIMEBOX,
|
||||
FIRST_ONBOARDING_PROMPT_TIMEOUT,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
} from '../constants';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
import { showMessage } from '@/components/mixins/showMessage';
|
||||
|
@ -115,6 +118,7 @@ import Modal from './Modal.vue';
|
|||
import { IFormInput, IFormInputs, IPersonalizationSurveyAnswersV2 } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { getAccountAge } from '@/modules/userHelpers';
|
||||
|
||||
export default mixins(showMessage, workflowHelpers).extend({
|
||||
components: { Modal },
|
||||
|
@ -135,6 +139,12 @@ export default mixins(showMessage, workflowHelpers).extend({
|
|||
...mapGetters({
|
||||
baseUrl: 'getBaseUrl',
|
||||
}),
|
||||
...mapGetters('users', [
|
||||
'currentUser',
|
||||
]),
|
||||
...mapGetters('settings', [
|
||||
'isOnboardingCallPromptFeatureEnabled',
|
||||
]),
|
||||
survey() {
|
||||
const survey: IFormInputs = [
|
||||
{
|
||||
|
@ -500,6 +510,7 @@ export default mixins(showMessage, workflowHelpers).extend({
|
|||
this.closeDialog();
|
||||
}
|
||||
|
||||
await this.fetchOnboardingPrompt();
|
||||
this.submitted = true;
|
||||
} catch (e) {
|
||||
this.$showError(e, 'Error while submitting results');
|
||||
|
@ -507,6 +518,33 @@ export default mixins(showMessage, workflowHelpers).extend({
|
|||
|
||||
this.$data.isSaving = false;
|
||||
},
|
||||
async fetchOnboardingPrompt() {
|
||||
if (this.isOnboardingCallPromptFeatureEnabled && getAccountAge(this.currentUser) <= ONBOARDING_PROMPT_TIMEBOX) {
|
||||
const onboardingResponse = await this.$store.dispatch('ui/getNextOnboardingPrompt');
|
||||
const promptTimeout = onboardingResponse.toast_sequence_number === 1 ? FIRST_ONBOARDING_PROMPT_TIMEOUT : 1000;
|
||||
|
||||
if (onboardingResponse.title && onboardingResponse.description) {
|
||||
setTimeout(async () => {
|
||||
this.$showToast({
|
||||
type: 'info',
|
||||
title: onboardingResponse.title,
|
||||
message: onboardingResponse.description,
|
||||
duration: 0,
|
||||
customClass: 'clickable',
|
||||
closeOnClick: true,
|
||||
onClick: () => {
|
||||
this.$telemetry.track('user clicked onboarding toast', {
|
||||
seq_num: onboardingResponse.toast_sequence_number,
|
||||
title: onboardingResponse.title,
|
||||
description: onboardingResponse.description,
|
||||
});
|
||||
this.$store.commit('ui/openModal', ONBOARDING_CALL_SIGNUP_MODAL_KEY, {root: true});
|
||||
},
|
||||
});
|
||||
}, promptTimeout);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -25,6 +25,17 @@
|
|||
</i>
|
||||
<span slot="title">{{ $locale.baseText('settings.n8napi') }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item
|
||||
v-for="fakeDoor in settingsFakeDoorFeatures"
|
||||
v-bind:key="fakeDoor.featureName"
|
||||
:index="`/settings/coming-soon/${fakeDoor.id}`"
|
||||
:class="$style.tab"
|
||||
>
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon :icon="fakeDoor.icon" />
|
||||
</i>
|
||||
<span slot="title">{{ $locale.baseText(fakeDoor.featureName) }}</span>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="/settings/community-nodes" v-if="canAccessCommunityNodes()" :class="$style.tab">
|
||||
<i :class="$style.icon">
|
||||
<font-awesome-icon icon="cube" />
|
||||
|
@ -45,6 +56,7 @@ import mixins from 'vue-typed-mixins';
|
|||
import { mapGetters } from 'vuex';
|
||||
import { ABOUT_MODAL_KEY, VIEWS } from '@/constants';
|
||||
import { userHelpers } from './mixins/userHelpers';
|
||||
import { IFakeDoor } from '@/Interface';
|
||||
|
||||
export default mixins(
|
||||
userHelpers,
|
||||
|
@ -52,6 +64,9 @@ export default mixins(
|
|||
name: 'SettingsSidebar',
|
||||
computed: {
|
||||
...mapGetters('settings', ['versionCli']),
|
||||
settingsFakeDoorFeatures(): IFakeDoor[] {
|
||||
return this.$store.getters['ui/getFakeDoorByLocation']('settings');
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
canAccessPersonalSettings(): boolean {
|
||||
|
|
|
@ -40,6 +40,7 @@ export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
|
|||
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
|
||||
export const EXECUTIONS_MODAL_KEY = 'executions';
|
||||
export const WORKFLOW_ACTIVE_MODAL_KEY = 'activation';
|
||||
export const ONBOARDING_CALL_SIGNUP_MODAL_KEY = 'onboardingCallSignup';
|
||||
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
|
||||
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
|
||||
|
||||
|
@ -55,7 +56,6 @@ export const BREAKPOINT_MD = 992;
|
|||
export const BREAKPOINT_LG = 1200;
|
||||
export const BREAKPOINT_XL = 1920;
|
||||
|
||||
|
||||
export const N8N_IO_BASE_URL = `https://api.n8n.io/api/`;
|
||||
export const DATA_PINNING_DOCS_URL = 'https://docs.n8n.io/data/data-pinning/';
|
||||
export const NPM_COMMUNITY_NODE_SEARCH_API_URL = `https://api.npms.io/v2/`;
|
||||
|
@ -275,9 +275,19 @@ export enum VIEWS {
|
|||
PERSONAL_SETTINGS = "PersonalSettings",
|
||||
API_SETTINGS = "APISettings",
|
||||
NOT_FOUND = "NotFoundView",
|
||||
FAKE_DOOR = "ComingSoon",
|
||||
COMMUNITY_NODES = "CommunityNodes",
|
||||
}
|
||||
|
||||
export enum FAKE_DOOR_FEATURES {
|
||||
ENVIRONMENTS = 'environments',
|
||||
LOGGING = 'logging',
|
||||
SHARING = 'sharing',
|
||||
}
|
||||
|
||||
export const ONBOARDING_PROMPT_TIMEBOX = 14;
|
||||
export const FIRST_ONBOARDING_PROMPT_TIMEOUT = 300000;
|
||||
|
||||
export const TEST_PIN_DATA = [
|
||||
{
|
||||
name: "First item",
|
||||
|
|
|
@ -30,6 +30,7 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
latestVersion: 0,
|
||||
path: '/',
|
||||
},
|
||||
onboardingCallPromptEnabled: false,
|
||||
},
|
||||
getters: {
|
||||
versionCli(state: ISettingsState) {
|
||||
|
@ -83,6 +84,9 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
templatesHost: (state): string => {
|
||||
return state.settings.templates.host;
|
||||
},
|
||||
isOnboardingCallPromptFeatureEnabled: (state): boolean => {
|
||||
return state.onboardingCallPromptEnabled;
|
||||
},
|
||||
isCommunityNodesFeatureEnabled: (state): boolean => {
|
||||
return state.settings.communityNodesEnabled;
|
||||
},
|
||||
|
@ -99,6 +103,7 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
state.api.enabled = settings.publicApi.enabled;
|
||||
state.api.latestVersion = settings.publicApi.latestVersion;
|
||||
state.api.path = settings.publicApi.path;
|
||||
state.onboardingCallPromptEnabled = settings.onboardingCallPromptEnabled;
|
||||
},
|
||||
stopShowingSetupPage(state: ISettingsState) {
|
||||
Vue.set(state.userManagement, 'showSetupOnFirstLoad', false);
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { applyForOnboardingCall, fetchNextOnboardingPrompt, submitEmailOnSignup } from '@/api/workflow-webhooks';
|
||||
import {
|
||||
ABOUT_MODAL_KEY,
|
||||
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
||||
|
@ -19,11 +20,15 @@ import {
|
|||
WORKFLOW_OPEN_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
VIEWS,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
FAKE_DOOR_FEATURES,
|
||||
COMMUNITY_PACKAGE_MANAGE_ACTIONS,
|
||||
} from '@/constants';
|
||||
import Vue from 'vue';
|
||||
import { ActionContext, Module } from 'vuex';
|
||||
import {
|
||||
IFakeDoor,
|
||||
IFakeDoorLocation,
|
||||
IRootState,
|
||||
IRunDataDisplayMode,
|
||||
IUiState,
|
||||
|
@ -61,6 +66,9 @@ const module: Module<IUiState, IRootState> = {
|
|||
[DUPLICATE_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[ONBOARDING_CALL_SIGNUP_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
[PERSONALIZATION_MODAL_KEY]: {
|
||||
open: false,
|
||||
},
|
||||
|
@ -117,6 +125,36 @@ const module: Module<IUiState, IRootState> = {
|
|||
mappingTelemetry: {},
|
||||
},
|
||||
mainPanelPosition: 0.5,
|
||||
fakeDoorFeatures: [
|
||||
{
|
||||
id: FAKE_DOOR_FEATURES.ENVIRONMENTS,
|
||||
featureName: 'fakeDoor.settings.environments.name',
|
||||
icon: 'server',
|
||||
infoText: 'fakeDoor.settings.environments.infoText',
|
||||
actionBoxTitle: 'fakeDoor.settings.environments.actionBox.title',
|
||||
actionBoxDescription: 'fakeDoor.settings.environments.actionBox.description',
|
||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=environments',
|
||||
uiLocations: ['settings'],
|
||||
},
|
||||
{
|
||||
id: FAKE_DOOR_FEATURES.LOGGING,
|
||||
featureName: 'fakeDoor.settings.logging.name',
|
||||
icon: 'sign-in-alt',
|
||||
infoText: 'fakeDoor.settings.logging.infoText',
|
||||
actionBoxTitle: 'fakeDoor.settings.logging.actionBox.title',
|
||||
actionBoxDescription: 'fakeDoor.settings.logging.actionBox.description',
|
||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=logging',
|
||||
uiLocations: ['settings'],
|
||||
},
|
||||
{
|
||||
id: FAKE_DOOR_FEATURES.SHARING,
|
||||
featureName: 'fakeDoor.credentialEdit.sharing.name',
|
||||
actionBoxTitle: 'fakeDoor.credentialEdit.sharing.actionBox.title',
|
||||
actionBoxDescription: 'fakeDoor.credentialEdit.sharing.actionBox.description',
|
||||
linkURL: 'https://n8n-community.typeform.com/to/l7QOrERN#f=sharing',
|
||||
uiLocations: ['credentialsModal'],
|
||||
},
|
||||
],
|
||||
draggable: {
|
||||
isDragging: false,
|
||||
type: '',
|
||||
|
@ -153,6 +191,13 @@ const module: Module<IUiState, IRootState> = {
|
|||
outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode,
|
||||
outputPanelEditMode: (state: IUiState): IUiState['ndv']['output']['editMode'] => state.ndv.output.editMode,
|
||||
mainPanelPosition: (state: IUiState) => state.mainPanelPosition,
|
||||
getFakeDoorFeatures: (state: IUiState) => state.fakeDoorFeatures,
|
||||
getFakeDoorByLocation: (state: IUiState) => (location: IFakeDoorLocation) => {
|
||||
return state.fakeDoorFeatures.filter(fakeDoor => fakeDoor.uiLocations.includes(location));
|
||||
},
|
||||
getFakeDoorById: (state: IUiState) => (id: string) => {
|
||||
return state.fakeDoorFeatures.find(fakeDoor => fakeDoor.id.toString() === id);
|
||||
},
|
||||
focusedMappableInput: (state: IUiState) => state.ndv.focusedMappableInput,
|
||||
isDraggableDragging: (state: IUiState) => state.draggable.isDragging,
|
||||
draggableType: (state: IUiState) => state.draggable.type,
|
||||
|
@ -264,6 +309,21 @@ const module: Module<IUiState, IRootState> = {
|
|||
context.commit('setMode', { name: CREDENTIAL_EDIT_MODAL_KEY, mode: 'new' });
|
||||
context.commit('openModal', CREDENTIAL_EDIT_MODAL_KEY);
|
||||
},
|
||||
getNextOnboardingPrompt: async (context: ActionContext<IUiState, IRootState>) => {
|
||||
const instanceId = context.rootGetters.instanceId;
|
||||
const currentUser = context.rootGetters['users/currentUser'];
|
||||
return await fetchNextOnboardingPrompt(instanceId, currentUser);
|
||||
},
|
||||
applyForOnboardingCall: async (context: ActionContext<IUiState, IRootState>, { email }) => {
|
||||
const instanceId = context.rootGetters.instanceId;
|
||||
const currentUser = context.rootGetters['users/currentUser'];
|
||||
return await applyForOnboardingCall(instanceId, currentUser, email);
|
||||
},
|
||||
submitContactEmail: async (context: ActionContext<IUiState, IRootState>, { email, agree }) => {
|
||||
const instanceId = context.rootGetters.instanceId;
|
||||
const currentUser = context.rootGetters['users/currentUser'];
|
||||
return await submitEmailOnSignup(instanceId, currentUser, email, agree);
|
||||
},
|
||||
async openCommunityPackageUninstallConfirmModal(context: ActionContext<IUiState, IRootState>, packageName: string) {
|
||||
context.commit('setActiveId', { name: COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, id: packageName});
|
||||
context.commit('setMode', { name: COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY, mode: COMMUNITY_PACKAGE_MANAGE_ACTIONS.UNINSTALL });
|
||||
|
|
|
@ -108,6 +108,16 @@ export function getPersonalizedNodeTypes(answers: IPersonalizationSurveyAnswersV
|
|||
return getPersonalizationV1(answers as IPersonalizationSurveyAnswersV1);
|
||||
}
|
||||
|
||||
export function getAccountAge(currentUser: IUser): number {
|
||||
if(currentUser.createdAt) {
|
||||
const accountCreatedAt = new Date(currentUser.createdAt);
|
||||
const today = new Date();
|
||||
|
||||
return Math.ceil((today.getTime() - accountCreatedAt.getTime()) / (1000* 3600 * 24));
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function getPersonalizationV2(answers: IPersonalizationSurveyAnswersV2) {
|
||||
let nodeTypes: string[] = [];
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
"generic.clickToCopy": "Click to copy",
|
||||
"generic.copiedToClipboard": "Copied to clipboard",
|
||||
"generic.beta": "beta",
|
||||
"generic.yes": "Yes",
|
||||
"generic.no": "No",
|
||||
"about.aboutN8n": "About n8n",
|
||||
"about.close": "Close",
|
||||
"about.license": "License",
|
||||
|
@ -55,6 +57,7 @@
|
|||
"auth.role": "Role",
|
||||
"auth.roles.member": "Member",
|
||||
"auth.roles.owner": "Owner",
|
||||
"auth.agreement.label": "I’d be OK sharing my opinion on n8n (no marketing emails though)",
|
||||
"auth.setup.confirmOwnerSetup": "Set up owner account?",
|
||||
"auth.setup.confirmOwnerSetupMessage": "To give others access to your <b>{entities}</b>, you’ll need to share these account details with them. Or you can continue as before with no account, by going back and skipping this setup. <a href=\"https://docs.n8n.io/reference/user-management.html\" target=\"_blank\">More info</a>",
|
||||
"auth.setup.createAccount": "Create account",
|
||||
|
@ -248,6 +251,18 @@
|
|||
"expressionEdit.expression": "Expression",
|
||||
"expressionEdit.result": "Result",
|
||||
"expressionEdit.variableSelector": "Variable Selector",
|
||||
"fakeDoor.credentialEdit.sharing.name": "Sharing",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.title": "We're working on sharing (as a paid feature)",
|
||||
"fakeDoor.credentialEdit.sharing.actionBox.description": "If you'd like to be the first to hear when it's ready, join the list",
|
||||
"fakeDoor.settings.environments.name": "Environments",
|
||||
"fakeDoor.settings.environments.infoText": "Environments allow you to use different settings and credentials in a workflow when you're building it vs when it's running in production",
|
||||
"fakeDoor.settings.environments.actionBox.title": "We're working on this (as a paid feature)",
|
||||
"fakeDoor.settings.environments.actionBox.description": "If you'd like to be the first to hear when it's ready, join the list.",
|
||||
"fakeDoor.settings.logging.name": "Logging",
|
||||
"fakeDoor.settings.logging.infoText": "You can already write logs to a file or the console using environment variables. <a href=\"https://docs.n8n.io/hosting/logging/\" target=\"_blank\">More info</a>",
|
||||
"fakeDoor.settings.logging.actionBox.title": "We're working on advanced logging (as a paid feature)",
|
||||
"fakeDoor.settings.logging.actionBox.description": "This also includes audit logging. If you'd like to be the first to hear when it's ready, join the list.",
|
||||
"fakeDoor.actionBox.button.label": "Join the list",
|
||||
"fixedCollectionParameter.choose": "Choose...",
|
||||
"fixedCollectionParameter.currentlyNoItemsExist": "Currently no items exist",
|
||||
"fixedCollectionParameter.deleteItem": "Delete item",
|
||||
|
@ -556,6 +571,17 @@
|
|||
"nodeWebhooks.showMessage.title": "URL copied",
|
||||
"nodeWebhooks.testUrl": "Test URL",
|
||||
"nodeWebhooks.webhookUrls": "Webhook URLs",
|
||||
"onboardingCallSignupModal.title": "Your onboarding session",
|
||||
"onboardingCallSignupModal.description": "Pop in your email and we'll send you some scheduling options",
|
||||
"onboardingCallSignupModal.emailInput.placeholder": "Your work email",
|
||||
"onboardingCallSignupModal.signupButton.label": "Submit",
|
||||
"onboardingCallSignupModal.cancelButton.label": "Cancel",
|
||||
"onboardingCallSignupModal.infoText.emailError": "This doesn't seem to be a valid email address",
|
||||
"onboardingCallSignupSucess.title": "Successfully signed up for an onboarding session",
|
||||
"onboardingCallSignupSucess.message": "You should receive a message from us shortly",
|
||||
"onboardingCallSignupFailed.title": "Something went wrong",
|
||||
"onboardingCallSignupFailed.message": "Your request could not be sent",
|
||||
"onboardingCallSignupModal.confirmExit.title": "Are you sure?",
|
||||
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts \n\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=RpjQTGKm-ok)",
|
||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||
|
|
|
@ -10,6 +10,7 @@ import SettingsPersonalView from './views/SettingsPersonalView.vue';
|
|||
import SettingsUsersView from './views/SettingsUsersView.vue';
|
||||
import SettingsCommunityNodesView from './views/SettingsCommunityNodesView.vue';
|
||||
import SettingsApiView from './views/SettingsApiView.vue';
|
||||
import SettingsFakeDoorView from './views/SettingsFakeDoorView.vue';
|
||||
import SetupView from './views/SetupView.vue';
|
||||
import SigninView from './views/SigninView.vue';
|
||||
import SignupView from './views/SignupView.vue';
|
||||
|
@ -328,6 +329,11 @@ const router = new Router({
|
|||
meta: {
|
||||
telemetry: {
|
||||
pageCategory: 'settings',
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
feature: 'users',
|
||||
};
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
allow: {
|
||||
|
@ -350,6 +356,11 @@ const router = new Router({
|
|||
meta: {
|
||||
telemetry: {
|
||||
pageCategory: 'settings',
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
feature: 'personal',
|
||||
};
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
allow: {
|
||||
|
@ -370,6 +381,11 @@ const router = new Router({
|
|||
meta: {
|
||||
telemetry: {
|
||||
pageCategory: 'settings',
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
feature: 'api',
|
||||
};
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
allow: {
|
||||
|
@ -405,6 +421,27 @@ const router = new Router({
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/settings/coming-soon/:featureId',
|
||||
name: VIEWS.FAKE_DOOR,
|
||||
component: SettingsFakeDoorView,
|
||||
props: true,
|
||||
meta: {
|
||||
telemetry: {
|
||||
pageCategory: 'settings',
|
||||
getProperties(route: Route, store: Store<IRootState>) {
|
||||
return {
|
||||
feature: route.params['featureId'],
|
||||
};
|
||||
},
|
||||
},
|
||||
permissions: {
|
||||
allow: {
|
||||
loginStatus: [LOGIN_STATUS.LoggedIn],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
name: VIEWS.NOT_FOUND,
|
||||
|
@ -422,6 +459,7 @@ const router = new Router({
|
|||
},
|
||||
permissions: {
|
||||
allow: {
|
||||
// TODO: Once custom permissions are merged, this needs to be updated with index validation
|
||||
loginStatus: [LOGIN_STATUS.LoggedIn, LOGIN_STATUS.LoggedOut],
|
||||
},
|
||||
},
|
||||
|
|
|
@ -84,3 +84,10 @@ body {
|
|||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.el-checkbox__label span {
|
||||
font-size: var(--font-size-2xs) !important;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
|
|
@ -154,7 +154,25 @@ import {
|
|||
} from 'jsplumb';
|
||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
||||
import { DEFAULT_STICKY_HEIGHT, DEFAULT_STICKY_WIDTH, MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, QUICKSTART_NOTE_NAME, START_NODE_TYPE, STICKY_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
import {
|
||||
DEFAULT_STICKY_HEIGHT,
|
||||
DEFAULT_STICKY_WIDTH,
|
||||
FIRST_ONBOARDING_PROMPT_TIMEOUT,
|
||||
MODAL_CANCEL,
|
||||
MODAL_CLOSE,
|
||||
MODAL_CONFIRMED,
|
||||
NODE_NAME_PREFIX,
|
||||
NODE_OUTPUT_DEFAULT_KEY,
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
ONBOARDING_PROMPT_TIMEBOX,
|
||||
PLACEHOLDER_EMPTY_WORKFLOW_ID,
|
||||
QUICKSTART_NOTE_NAME,
|
||||
START_NODE_TYPE,
|
||||
STICKY_NODE_TYPE,
|
||||
VIEWS,
|
||||
WEBHOOK_NODE_TYPE,
|
||||
WORKFLOW_OPEN_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
|
@ -215,11 +233,12 @@ import { mapGetters } from 'vuex';
|
|||
|
||||
import {
|
||||
addNodeTranslation,
|
||||
addHeaders,
|
||||
} from '@/plugins/i18n';
|
||||
|
||||
import '../plugins/N8nCustomConnectorType';
|
||||
import '../plugins/PlusEndpointType';
|
||||
import { getAccountAge } from '@/modules/userHelpers';
|
||||
import { IUser } from 'n8n-design-system';
|
||||
import {dataPinningEventBus} from "@/event-bus/data-pinning-event-bus";
|
||||
|
||||
interface AddNodeOptions {
|
||||
|
@ -312,9 +331,15 @@ export default mixins(
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('users', [
|
||||
'currentUser',
|
||||
]),
|
||||
...mapGetters('ui', [
|
||||
'sidebarMenuCollapsed',
|
||||
]),
|
||||
...mapGetters('settings', [
|
||||
'isOnboardingCallPromptFeatureEnabled',
|
||||
]),
|
||||
defaultLocale (): string {
|
||||
return this.$store.getters.defaultLocale;
|
||||
},
|
||||
|
@ -3060,6 +3085,35 @@ export default mixins(
|
|||
|
||||
this.$externalHooks().run('nodeView.mount');
|
||||
|
||||
if (
|
||||
this.currentUser.personalizationAnswers !== null &&
|
||||
this.isOnboardingCallPromptFeatureEnabled &&
|
||||
getAccountAge(this.currentUser) <= ONBOARDING_PROMPT_TIMEBOX
|
||||
) {
|
||||
const onboardingResponse = await this.$store.dispatch('ui/getNextOnboardingPrompt');
|
||||
const promptTimeout = onboardingResponse.toast_sequence_number === 1 ? FIRST_ONBOARDING_PROMPT_TIMEOUT : 1000;
|
||||
|
||||
if (onboardingResponse.title && onboardingResponse.description) {
|
||||
setTimeout(async () => {
|
||||
this.$showToast({
|
||||
type: 'info',
|
||||
title: onboardingResponse.title,
|
||||
message: onboardingResponse.description,
|
||||
duration: 0,
|
||||
customClass: 'clickable',
|
||||
closeOnClick: true,
|
||||
onClick: () => {
|
||||
this.$telemetry.track('user clicked onboarding toast', {
|
||||
seq_num: onboardingResponse.toast_sequence_number,
|
||||
title: onboardingResponse.title,
|
||||
description: onboardingResponse.description,
|
||||
});
|
||||
this.$store.commit('ui/openModal', ONBOARDING_CALL_SIGNUP_MODAL_KEY, {root: true});
|
||||
},
|
||||
});
|
||||
}, promptTimeout);
|
||||
}
|
||||
}
|
||||
dataPinningEventBus.$on('pin-data', this.addPinDataConnections);
|
||||
dataPinningEventBus.$on('unpin-data', this.removePinDataConnections);
|
||||
},
|
||||
|
|
39
packages/editor-ui/src/views/SettingsFakeDoorView.vue
Normal file
39
packages/editor-ui/src/views/SettingsFakeDoorView.vue
Normal file
|
@ -0,0 +1,39 @@
|
|||
<template>
|
||||
<SettingsView>
|
||||
<FeatureComingSoon :featureId="featureId"></FeatureComingSoon>
|
||||
</SettingsView>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { IFakeDoor } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
import SettingsView from './SettingsView.vue';
|
||||
import FeatureComingSoon from '../components/FeatureComingSoon.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SettingsFakeDoorView',
|
||||
components: {
|
||||
SettingsView,
|
||||
FeatureComingSoon,
|
||||
},
|
||||
props: {
|
||||
featureId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
featureInfo(): IFakeDoor {
|
||||
return this.$store.getters['ui/getFakeDoorFeatures'][this.featureId] as IFakeDoor;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openLinkPage() {
|
||||
window.open(this.featureInfo.linkURL, '_blank');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
</style>
|
|
@ -79,6 +79,13 @@ export default mixins(
|
|||
capitalize: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'agree',
|
||||
properties: {
|
||||
label: this.$locale.baseText('auth.agreement.label'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
@ -130,7 +137,7 @@ export default mixins(
|
|||
this.$locale.baseText('auth.setup.goBack'),
|
||||
);
|
||||
},
|
||||
async onSubmit(values: {[key: string]: string}) {
|
||||
async onSubmit(values: {[key: string]: string | boolean}) {
|
||||
try {
|
||||
const confirmSetup = await this.confirmSetupOrGoBack();
|
||||
if (!confirmSetup) {
|
||||
|
@ -140,6 +147,13 @@ export default mixins(
|
|||
const forceRedirectedHere = this.$store.getters['settings/showSetupPage'];
|
||||
this.loading = true;
|
||||
await this.$store.dispatch('users/createOwner', values);
|
||||
|
||||
if (values.agree === true) {
|
||||
try {
|
||||
await this.$store.dispatch('ui/submitContactEmail', { email: values.email, agree: values.agree });
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (forceRedirectedHere) {
|
||||
await this.$router.push({ name: VIEWS.HOMEPAGE });
|
||||
}
|
||||
|
|
|
@ -59,6 +59,13 @@ export default mixins(
|
|||
capitalize: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'agree',
|
||||
properties: {
|
||||
label: this.$locale.baseText('auth.agreement.label'),
|
||||
type: 'checkbox',
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
return {
|
||||
|
@ -95,13 +102,19 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
async onSubmit(values: {[key: string]: string}) {
|
||||
async onSubmit(values: {[key: string]: string | boolean}) {
|
||||
try {
|
||||
this.loading = true;
|
||||
const inviterId = this.$route.query.inviterId;
|
||||
const inviteeId = this.$route.query.inviteeId;
|
||||
await this.$store.dispatch('users/signup', {...values, inviterId, inviteeId});
|
||||
|
||||
if (values.agree === true) {
|
||||
try {
|
||||
await this.$store.dispatch('ui/submitContactEmail', { email: values.email, agree: values.agree });
|
||||
} catch { }
|
||||
}
|
||||
|
||||
await this.$router.push({ name: VIEWS.HOMEPAGE });
|
||||
} catch (error) {
|
||||
this.$showError(error, this.$locale.baseText('auth.signup.setupYourAccountError'));
|
||||
|
|
Loading…
Reference in a new issue