feat: Update NPS Value Survey (#9638)
Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in> Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
@@ -742,18 +742,9 @@ export interface IUserListAction {
|
||||
}
|
||||
|
||||
export interface IN8nPrompts {
|
||||
message: string;
|
||||
title: string;
|
||||
showContactPrompt: boolean;
|
||||
showValueSurvey: boolean;
|
||||
}
|
||||
|
||||
export interface IN8nValueSurveyData {
|
||||
[key: string]: string;
|
||||
}
|
||||
|
||||
export interface IN8nPromptResponse {
|
||||
updated: boolean;
|
||||
message?: string;
|
||||
title?: string;
|
||||
showContactPrompt?: boolean;
|
||||
}
|
||||
|
||||
export const enum UserManagementAuthenticationMethod {
|
||||
@@ -1214,6 +1205,8 @@ export type Modals = {
|
||||
[key: string]: ModalState;
|
||||
};
|
||||
|
||||
export type ModalKey = keyof Modals;
|
||||
|
||||
export type ModalState = {
|
||||
open: boolean;
|
||||
mode?: string | null;
|
||||
@@ -1366,7 +1359,6 @@ export interface INodeCreatorState {
|
||||
export interface ISettingsState {
|
||||
initialized: boolean;
|
||||
settings: IN8nUISettings;
|
||||
promptsData: IN8nPrompts;
|
||||
userManagement: IUserManagementSettings;
|
||||
templatesEndpointHealthy: boolean;
|
||||
api: {
|
||||
@@ -1931,3 +1923,7 @@ export type EnterpriseEditionFeatureKey =
|
||||
| 'AdvancedPermissions';
|
||||
|
||||
export type EnterpriseEditionFeatureValue = keyof Omit<IN8nUISettings['enterprise'], 'projects'>;
|
||||
|
||||
export interface IN8nPromptResponse {
|
||||
updated: boolean;
|
||||
}
|
||||
|
||||
@@ -39,12 +39,6 @@ export const waitAllPromises = async () => await new Promise((resolve) => setTim
|
||||
export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
|
||||
initialized: true,
|
||||
settings: defaultSettings,
|
||||
promptsData: {
|
||||
message: '',
|
||||
title: '',
|
||||
showContactPrompt: false,
|
||||
showValueSurvey: false,
|
||||
},
|
||||
userManagement: {
|
||||
showSetupOnFirstLoad: false,
|
||||
smtpSetup: false,
|
||||
|
||||
7
packages/editor-ui/src/api/npsSurvey.ts
Normal file
7
packages/editor-ui/src/api/npsSurvey.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { IRestApiContext } from '@/Interface';
|
||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
import type { NpsSurveyState } from 'n8n-workflow';
|
||||
|
||||
export async function updateNpsSurveyState(context: IRestApiContext, state: NpsSurveyState) {
|
||||
await makeRestApiRequest(context, 'PATCH', '/user-settings/nps-survey', state);
|
||||
}
|
||||
@@ -1,9 +1,4 @@
|
||||
import type {
|
||||
IRestApiContext,
|
||||
IN8nPrompts,
|
||||
IN8nValueSurveyData,
|
||||
IN8nPromptResponse,
|
||||
} from '../Interface';
|
||||
import type { IRestApiContext, IN8nPrompts, IN8nPromptResponse } from '../Interface';
|
||||
import { makeRestApiRequest, get, post } from '@/utils/apiUtils';
|
||||
import { N8N_IO_BASE_URL, NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@/constants';
|
||||
import type { IN8nUISettings } from 'n8n-workflow';
|
||||
@@ -34,17 +29,6 @@ export async function submitContactInfo(
|
||||
);
|
||||
}
|
||||
|
||||
export async function submitValueSurvey(
|
||||
instanceId: string,
|
||||
userId: string,
|
||||
params: IN8nValueSurveyData,
|
||||
): Promise<IN8nPromptResponse> {
|
||||
return await post(N8N_IO_BASE_URL, '/value-survey', params, {
|
||||
'n8n-instance-id': instanceId,
|
||||
'n8n-user-id': userId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAvailableCommunityPackageCount(): Promise<number> {
|
||||
const response = await get(
|
||||
NPM_COMMUNITY_NODE_SEARCH_API_URL,
|
||||
|
||||
@@ -33,20 +33,27 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { IN8nPromptResponse } from '@/Interface';
|
||||
import type { IN8nPromptResponse, ModalKey } from '@/Interface';
|
||||
import { VALID_EMAIL_REGEX } from '@/constants';
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ContactPromptModal',
|
||||
components: { Modal },
|
||||
props: ['modalName'],
|
||||
props: {
|
||||
modalName: {
|
||||
type: String as PropType<ModalKey>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
...useToast(),
|
||||
@@ -59,17 +66,17 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore),
|
||||
...mapStores(useRootStore, useSettingsStore, useNpsSurveyStore),
|
||||
title(): string {
|
||||
if (this.settingsStore.promptsData && this.settingsStore.promptsData.title) {
|
||||
return this.settingsStore.promptsData.title;
|
||||
if (this.npsSurveyStore.promptsData?.title) {
|
||||
return this.npsSurveyStore.promptsData.title;
|
||||
}
|
||||
|
||||
return 'You’re a power user 💪';
|
||||
},
|
||||
description(): string {
|
||||
if (this.settingsStore.promptsData && this.settingsStore.promptsData.message) {
|
||||
return this.settingsStore.promptsData.message;
|
||||
if (this.npsSurveyStore.promptsData?.message) {
|
||||
return this.npsSurveyStore.promptsData.message;
|
||||
}
|
||||
|
||||
return 'Your experience with n8n can help us improve — for you and our entire community.';
|
||||
|
||||
@@ -55,6 +55,7 @@ import type {
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import type { BaseTextKey } from '../../plugins/i18n';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
const props = defineProps<{
|
||||
workflow: IWorkflowDb;
|
||||
@@ -72,6 +73,7 @@ const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
const npsSurveyStore = useNpsSurveyStore();
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
@@ -250,7 +252,7 @@ async function onSaveButtonClick() {
|
||||
if (saved) {
|
||||
showCreateWorkflowSuccessToast(id);
|
||||
|
||||
await settingsStore.fetchPromptsData();
|
||||
await npsSurveyStore.fetchPromptsData();
|
||||
|
||||
if (route.name === VIEWS.EXECUTION_DEBUG) {
|
||||
await router.replace({
|
||||
|
||||
@@ -54,13 +54,14 @@ import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { EventBus } from 'n8n-design-system';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import type { ModalKey } from '@/Interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Modal',
|
||||
props: {
|
||||
...ElDialog.props,
|
||||
name: {
|
||||
type: String,
|
||||
type: String as PropType<ModalKey>,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
|
||||
@@ -12,15 +12,17 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { ModalKey } from '@/Interface';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ModalRoot',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
type: String as PropType<ModalKey>,
|
||||
required: true,
|
||||
},
|
||||
keepAlive: {
|
||||
|
||||
@@ -41,9 +41,9 @@
|
||||
<UpdatesPanel />
|
||||
</ModalRoot>
|
||||
|
||||
<ModalRoot :name="VALUE_SURVEY_MODAL_KEY" :keep-alive="true">
|
||||
<ModalRoot :name="NPS_SURVEY_MODAL_KEY" :keep-alive="true">
|
||||
<template #default="{ active }">
|
||||
<ValueSurvey :is-active="active" />
|
||||
<NpsSurvey :is-active="active" />
|
||||
</template>
|
||||
</ModalRoot>
|
||||
|
||||
@@ -187,7 +187,7 @@ import {
|
||||
ONBOARDING_CALL_SIGNUP_MODAL_KEY,
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
@@ -220,7 +220,7 @@ import OnboardingCallSignupModal from './OnboardingCallSignupModal.vue';
|
||||
import PersonalizationModal from './PersonalizationModal.vue';
|
||||
import TagsManager from './TagsManager/TagsManager.vue';
|
||||
import UpdatesPanel from './UpdatesPanel.vue';
|
||||
import ValueSurvey from './ValueSurvey.vue';
|
||||
import NpsSurvey from './NpsSurvey.vue';
|
||||
import WorkflowLMChat from './WorkflowLMChat.vue';
|
||||
import WorkflowSettings from './WorkflowSettings.vue';
|
||||
import DeleteUserModal from './DeleteUserModal.vue';
|
||||
@@ -257,7 +257,7 @@ export default defineComponent({
|
||||
PersonalizationModal,
|
||||
TagsManager,
|
||||
UpdatesPanel,
|
||||
ValueSurvey,
|
||||
NpsSurvey,
|
||||
WorkflowLMChat,
|
||||
WorkflowSettings,
|
||||
WorkflowShareModal,
|
||||
@@ -291,7 +291,7 @@ export default defineComponent({
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
WORKFLOW_SHARE_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
IMPORT_CURL_MODAL_KEY,
|
||||
GENERATE_CURL_MODAL_KEY,
|
||||
|
||||
283
packages/editor-ui/src/components/NpsSurvey.vue
Normal file
283
packages/editor-ui/src/components/NpsSurvey.vue
Normal file
@@ -0,0 +1,283 @@
|
||||
<script lang="ts" setup>
|
||||
import { VALID_EMAIL_REGEX, NPS_SURVEY_MODAL_KEY } from '@/constants';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import ModalDrawer from '@/components/ModalDrawer.vue';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { ref, computed, watch } from 'vue';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
const props = defineProps({
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const rootStore = useRootStore();
|
||||
const i18n = useI18n();
|
||||
const toast = useToast();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const DEFAULT_TITLE = i18n.baseText('prompts.npsSurvey.recommendationQuestion');
|
||||
const GREAT_FEEDBACK_TITLE = i18n.baseText('prompts.npsSurvey.greatFeedbackTitle');
|
||||
const DEFAULT_FEEDBACK_TITLE = i18n.baseText('prompts.npsSurvey.defaultFeedbackTitle');
|
||||
const PRODUCT_TEAM_MESSAGE = i18n.baseText('prompts.productTeamMessage');
|
||||
const VERY_LIKELY_OPTION = i18n.baseText('prompts.npsSurvey.veryLikely');
|
||||
const NOT_LIKELY_OPTION = i18n.baseText('prompts.npsSurvey.notLikely');
|
||||
const SEND = i18n.baseText('prompts.npsSurvey.send');
|
||||
const YOUR_EMAIL_ADDRESS = i18n.baseText('prompts.npsSurvey.yourEmailAddress');
|
||||
|
||||
const form = ref<{ value: string; email: string }>({ value: '', email: '' });
|
||||
const showButtons = ref(true);
|
||||
const modalBus = createEventBus();
|
||||
|
||||
const modalTitle = computed(() => {
|
||||
if (form?.value?.value !== '') {
|
||||
if (Number(form.value) > 7) {
|
||||
return GREAT_FEEDBACK_TITLE;
|
||||
} else {
|
||||
return DEFAULT_FEEDBACK_TITLE;
|
||||
}
|
||||
}
|
||||
|
||||
return DEFAULT_TITLE;
|
||||
});
|
||||
|
||||
const isEmailValid = computed(
|
||||
() => form?.value?.email && VALID_EMAIL_REGEX.test(String(form.value.email).toLowerCase()),
|
||||
);
|
||||
|
||||
async function closeDialog(): Promise<void> {
|
||||
if (form.value.value === '') {
|
||||
telemetry.track('User responded value survey score', {
|
||||
instance_id: rootStore.instanceId,
|
||||
nps: '',
|
||||
});
|
||||
|
||||
await useNpsSurveyStore().ignoreNpsSurvey();
|
||||
}
|
||||
if (form.value.value !== '' && form.value.email === '') {
|
||||
telemetry.track('User responded value survey email', {
|
||||
instance_id: rootStore.instanceId,
|
||||
email: '',
|
||||
nps: form.value.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function onInputChange(value: string) {
|
||||
form.value.email = value;
|
||||
}
|
||||
|
||||
async function selectSurveyValue(value: string) {
|
||||
form.value.value = value;
|
||||
showButtons.value = false;
|
||||
|
||||
telemetry.track('User responded value survey score', {
|
||||
instance_id: rootStore.instanceId,
|
||||
nps: form.value.value,
|
||||
});
|
||||
|
||||
await useNpsSurveyStore().respondNpsSurvey();
|
||||
}
|
||||
|
||||
async function send() {
|
||||
if (isEmailValid.value) {
|
||||
telemetry.track('User responded value survey email', {
|
||||
instance_id: rootStore.instanceId,
|
||||
email: form.value.email,
|
||||
nps: form.value.value,
|
||||
});
|
||||
|
||||
toast.showMessage({
|
||||
title: i18n.baseText('prompts.npsSurvey.thanks'),
|
||||
message: Number(form.value.value) >= 8 ? i18n.baseText('prompts.npsSurvey.reviewUs') : '',
|
||||
type: 'success',
|
||||
duration: 15000,
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
form.value.value = '';
|
||||
form.value.email = '';
|
||||
showButtons.value = true;
|
||||
}, 1000);
|
||||
modalBus.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.isActive,
|
||||
(isActive) => {
|
||||
if (isActive) {
|
||||
telemetry.track('User shown value survey', {
|
||||
instance_id: rootStore.instanceId,
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalDrawer
|
||||
:name="NPS_SURVEY_MODAL_KEY"
|
||||
:event-bus="modalBus"
|
||||
:before-close="closeDialog"
|
||||
:modal="false"
|
||||
:wrapper-closable="false"
|
||||
direction="btt"
|
||||
width="120px"
|
||||
class="nps-survey"
|
||||
:class="$style.npsSurvey"
|
||||
data-test-id="nps-survey-modal"
|
||||
>
|
||||
<template #header>
|
||||
<div :class="$style.title">
|
||||
<n8n-heading tag="h2" size="medium" color="text-xlight">{{ modalTitle }}</n8n-heading>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<section :class="$style.content">
|
||||
<div v-if="showButtons" :class="$style.wrapper">
|
||||
<div :class="$style.buttons" data-test-id="nps-survey-ratings">
|
||||
<div v-for="value in 11" :key="value - 1" :class="$style.container">
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
:label="(value - 1).toString()"
|
||||
square
|
||||
@click="selectSurveyValue((value - 1).toString())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.text">
|
||||
<n8n-text size="small" color="text-xlight">{{ NOT_LIKELY_OPTION }}</n8n-text>
|
||||
<n8n-text size="small" color="text-xlight">{{ VERY_LIKELY_OPTION }}</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.email">
|
||||
<div :class="$style.input" @keyup.enter="send" data-test-id="nps-survey-email">
|
||||
<n8n-input
|
||||
v-model="form.email"
|
||||
:placeholder="YOUR_EMAIL_ADDRESS"
|
||||
@update:model-value="onInputChange"
|
||||
/>
|
||||
<div :class="$style.button">
|
||||
<n8n-button :label="SEND" float="right" :disabled="!isEmailValid" @click="send" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.disclaimer">
|
||||
<n8n-text size="small" color="text-dark">
|
||||
{{ PRODUCT_TEAM_MESSAGE }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</ModalDrawer>
|
||||
</template>
|
||||
|
||||
<style module lang="scss">
|
||||
.title {
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
margin-top: 10px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--color-nps-survey-font);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.text span {
|
||||
color: var(--color-nps-survey-font);
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 8px;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.npsSurvey {
|
||||
background: var(--color-nps-survey-background);
|
||||
height: 120px;
|
||||
top: auto;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
height: 140px !important;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 50px;
|
||||
margin: 0;
|
||||
padding: 18px 0 16px;
|
||||
|
||||
button {
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
position: absolute;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-nps-survey-font);
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,295 +0,0 @@
|
||||
<template>
|
||||
<ModalDrawer
|
||||
:name="VALUE_SURVEY_MODAL_KEY"
|
||||
:event-bus="modalBus"
|
||||
:before-close="closeDialog"
|
||||
:modal="false"
|
||||
:wrapper-closable="false"
|
||||
direction="btt"
|
||||
width="120px"
|
||||
:class="$style.valueSurvey"
|
||||
>
|
||||
<template #header>
|
||||
<div :class="$style.title">
|
||||
<n8n-heading tag="h2" size="medium" color="text-xlight">{{ getTitle }}</n8n-heading>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<section :class="$style.content">
|
||||
<div v-if="showButtons" :class="$style.wrapper">
|
||||
<div :class="$style.buttons">
|
||||
<div v-for="value in 11" :key="value - 1" :class="$style.container">
|
||||
<n8n-button
|
||||
type="tertiary"
|
||||
:label="(value - 1).toString()"
|
||||
square
|
||||
@click="selectSurveyValue((value - 1).toString())"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.text">
|
||||
<n8n-text size="small" color="text-xlight">Not likely</n8n-text>
|
||||
<n8n-text size="small" color="text-xlight">Very likely</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.email">
|
||||
<div :class="$style.input" @keyup.enter="send">
|
||||
<n8n-input
|
||||
v-model="form.email"
|
||||
placeholder="Your email address"
|
||||
@update:model-value="onInputChange"
|
||||
/>
|
||||
<div :class="$style.button">
|
||||
<n8n-button label="Send" float="right" :disabled="!isEmailValid" @click="send" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.disclaimer">
|
||||
<n8n-text size="small" color="text-xlight">
|
||||
David from our product team will get in touch personally
|
||||
</n8n-text>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</ModalDrawer>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import { VALID_EMAIL_REGEX, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||
import type { IN8nPromptResponse } from '@/Interface';
|
||||
|
||||
import ModalDrawer from '@/components/ModalDrawer.vue';
|
||||
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
|
||||
const DEFAULT_TITLE = 'How likely are you to recommend n8n to a friend or colleague?';
|
||||
const GREAT_FEEDBACK_TITLE =
|
||||
'Great to hear! Can we reach out to see how we can make n8n even better for you?';
|
||||
const DEFAULT_FEEDBACK_TITLE =
|
||||
"Thanks for your feedback! We'd love to understand how we can improve. Can we reach out?";
|
||||
|
||||
export default defineComponent({
|
||||
name: 'ValueSurvey',
|
||||
components: {
|
||||
ModalDrawer,
|
||||
},
|
||||
props: ['isActive'],
|
||||
setup() {
|
||||
return {
|
||||
...useToast(),
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
isActive(isActive) {
|
||||
if (isActive) {
|
||||
this.$telemetry.track('User shown value survey', {
|
||||
instance_id: this.rootStore.instanceId,
|
||||
});
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore),
|
||||
getTitle(): string {
|
||||
if (this.form.value !== '') {
|
||||
if (Number(this.form.value) > 7) {
|
||||
return GREAT_FEEDBACK_TITLE;
|
||||
} else {
|
||||
return DEFAULT_FEEDBACK_TITLE;
|
||||
}
|
||||
} else {
|
||||
return DEFAULT_TITLE;
|
||||
}
|
||||
},
|
||||
isEmailValid(): boolean {
|
||||
return VALID_EMAIL_REGEX.test(String(this.form.email).toLowerCase());
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
email: '',
|
||||
value: '',
|
||||
},
|
||||
showButtons: true,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
modalBus: createEventBus(),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
closeDialog(): void {
|
||||
if (this.form.value === '') {
|
||||
this.$telemetry.track('User responded value survey score', {
|
||||
instance_id: this.rootStore.instanceId,
|
||||
nps: '',
|
||||
});
|
||||
}
|
||||
if (this.form.value !== '' && this.form.email === '') {
|
||||
this.$telemetry.track('User responded value survey email', {
|
||||
instance_id: this.rootStore.instanceId,
|
||||
email: '',
|
||||
});
|
||||
}
|
||||
},
|
||||
onInputChange(value: string) {
|
||||
this.form.email = value;
|
||||
},
|
||||
async selectSurveyValue(value: string) {
|
||||
this.form.value = value;
|
||||
this.showButtons = false;
|
||||
|
||||
const response: IN8nPromptResponse | undefined = await this.settingsStore.submitValueSurvey({
|
||||
value: this.form.value,
|
||||
});
|
||||
|
||||
if (response && response.updated) {
|
||||
this.$telemetry.track('User responded value survey score', {
|
||||
instance_id: this.rootStore.instanceId,
|
||||
nps: this.form.value,
|
||||
});
|
||||
}
|
||||
},
|
||||
async send() {
|
||||
if (this.isEmailValid) {
|
||||
const response: IN8nPromptResponse | undefined = await this.settingsStore.submitValueSurvey(
|
||||
{
|
||||
email: this.form.email,
|
||||
value: this.form.value,
|
||||
},
|
||||
);
|
||||
|
||||
if (response && response.updated) {
|
||||
this.$telemetry.track('User responded value survey email', {
|
||||
instance_id: this.rootStore.instanceId,
|
||||
email: this.form.email,
|
||||
});
|
||||
this.showMessage({
|
||||
title: 'Thanks for your feedback',
|
||||
message:
|
||||
'If you’d like to help even more, leave us a <a target="_blank" href="https://www.g2.com/products/n8n/reviews/start">review on G2</a>.',
|
||||
type: 'success',
|
||||
duration: 15000,
|
||||
});
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
this.form.value = '';
|
||||
this.form.email = '';
|
||||
this.showButtons = true;
|
||||
}, 1000);
|
||||
this.modalBus.emit('close');
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.title {
|
||||
height: 16px;
|
||||
text-align: center;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
margin-top: 10px;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: var(--color-value-survey-font);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.text span {
|
||||
color: var(--color-value-survey-font);
|
||||
}
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.container {
|
||||
margin: 0 8px;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
margin-top: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.valueSurvey {
|
||||
background: var(--color-value-survey-background);
|
||||
height: 120px;
|
||||
top: auto;
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
height: 140px;
|
||||
}
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
height: 140px !important;
|
||||
}
|
||||
|
||||
header {
|
||||
height: 50px;
|
||||
margin: 0;
|
||||
padding: 18px 0 16px;
|
||||
|
||||
button {
|
||||
top: 12px;
|
||||
right: 16px;
|
||||
position: absolute;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-value-survey-font);
|
||||
|
||||
@media (max-width: $breakpoint-xs) {
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -47,6 +47,7 @@ import { PLACEHOLDER_EMPTY_WORKFLOW_ID, WORKFLOW_SETTINGS_MODAL_KEY } from '@/co
|
||||
import type { IWorkflowSettings } from 'n8n-workflow';
|
||||
import { deepCopy } from 'n8n-workflow';
|
||||
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
interface IWorkflowSaveSettings {
|
||||
saveFailedExecutions: boolean;
|
||||
@@ -85,7 +86,7 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useRootStore, useSettingsStore, useUIStore, useWorkflowsStore),
|
||||
...mapStores(useRootStore, useSettingsStore, useUIStore, useWorkflowsStore, useNpsSurveyStore),
|
||||
accordionItems(): object[] {
|
||||
return [
|
||||
{
|
||||
@@ -228,7 +229,9 @@ export default defineComponent({
|
||||
name: this.workflowName,
|
||||
tags: this.currentWorkflowTagIds,
|
||||
});
|
||||
if (saved) await this.settingsStore.fetchPromptsData();
|
||||
if (saved) {
|
||||
await this.npsSurveyStore.fetchPromptsData();
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -49,6 +49,7 @@ import { useTagsStore } from '@/stores/tags.store';
|
||||
import { executionFilterToQueryFilter } from '@/utils/executionUtils';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
export default defineComponent({
|
||||
name: 'WorkflowExecutionsList',
|
||||
@@ -79,7 +80,7 @@ export default defineComponent({
|
||||
if (confirmModal === MODAL_CONFIRM) {
|
||||
const saved = await this.workflowHelpers.saveCurrentWorkflow({}, false);
|
||||
if (saved) {
|
||||
await this.settingsStore.fetchPromptsData();
|
||||
await this.npsSurveyStore.fetchPromptsData();
|
||||
}
|
||||
this.uiStore.stateIsDirty = false;
|
||||
next();
|
||||
@@ -141,7 +142,7 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useTagsStore, useNodeTypesStore, useSettingsStore, useUIStore),
|
||||
...mapStores(useTagsStore, useNodeTypesStore, useSettingsStore, useUIStore, useNpsSurveyStore),
|
||||
temporaryExecution(): ExecutionSummary | undefined {
|
||||
const isTemporary = !this.executions.find((execution) => execution.id === this.execution?.id);
|
||||
return isTemporary ? this.execution : undefined;
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
import { useSettingsStore } from '@/stores/settings.store';
|
||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||
import { useRouter } from 'vue-router';
|
||||
@@ -15,6 +14,7 @@ import { useTelemetry } from '@/composables/useTelemetry';
|
||||
import { useToast } from '@/composables/useToast';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { ref } from 'vue';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
export function useWorkflowActivate() {
|
||||
const updatingWorkflowActivation = ref(false);
|
||||
@@ -22,11 +22,11 @@ export function useWorkflowActivate() {
|
||||
const router = useRouter();
|
||||
const workflowHelpers = useWorkflowHelpers({ router });
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const uiStore = useUIStore();
|
||||
const telemetry = useTelemetry();
|
||||
const toast = useToast();
|
||||
const i18n = useI18n();
|
||||
const npsSurveyStore = useNpsSurveyStore();
|
||||
|
||||
//methods
|
||||
|
||||
@@ -117,7 +117,7 @@ export function useWorkflowActivate() {
|
||||
if (newActiveState && useStorage(LOCAL_STORAGE_ACTIVATION_FLAG).value !== 'true') {
|
||||
uiStore.openModal(WORKFLOW_ACTIVE_MODAL_KEY);
|
||||
} else {
|
||||
await settingsStore.fetchPromptsData();
|
||||
await npsSurveyStore.fetchPromptsData();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -50,7 +50,7 @@ export const WORKFLOW_LM_CHAT_MODAL_KEY = 'lmChat';
|
||||
export const WORKFLOW_SHARE_MODAL_KEY = 'workflowShare';
|
||||
export const PERSONALIZATION_MODAL_KEY = 'personalization';
|
||||
export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
|
||||
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
|
||||
export const NPS_SURVEY_MODAL_KEY = 'npsSurvey';
|
||||
export const WORKFLOW_ACTIVE_MODAL_KEY = 'activation';
|
||||
export const ONBOARDING_CALL_SIGNUP_MODAL_KEY = 'onboardingCallSignup';
|
||||
export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
|
||||
@@ -767,6 +767,10 @@ export const TIME = {
|
||||
DAY: 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
export const THREE_DAYS_IN_MILLIS = 3 * TIME.DAY;
|
||||
export const SEVEN_DAYS_IN_MILLIS = 7 * TIME.DAY;
|
||||
export const SIX_MONTHS_IN_MILLIS = 6 * 30 * TIME.DAY;
|
||||
|
||||
/**
|
||||
* Mouse button codes
|
||||
*/
|
||||
|
||||
@@ -1449,6 +1449,16 @@
|
||||
"pushConnection.executionError": "There was a problem executing the workflow{error}",
|
||||
"pushConnection.executionError.openNode": " <a data-action='openNodeDetail' data-action-parameter-node='{node}'>Open node</a>",
|
||||
"pushConnection.executionError.details": "<br /><strong>{details}</strong>",
|
||||
"prompts.productTeamMessage": "Our product team will get in touch personally",
|
||||
"prompts.npsSurvey.recommendationQuestion": "How likely are you to recommend n8n to a friend or colleague?",
|
||||
"prompts.npsSurvey.greatFeedbackTitle": "Great to hear! Can we reach out to see how we can make n8n even better for you?",
|
||||
"prompts.npsSurvey.defaultFeedbackTitle": "Thanks for your feedback! We'd love to understand how we can improve. Can we reach out?",
|
||||
"prompts.npsSurvey.notLikely": "Not likely",
|
||||
"prompts.npsSurvey.veryLikely": "Very likely",
|
||||
"prompts.npsSurvey.send": "Send",
|
||||
"prompts.npsSurvey.yourEmailAddress": "Your email address",
|
||||
"prompts.npsSurvey.reviewUs": "If you’d like to help even more, leave us a <a target=\"_blank\" href=\"https://www.g2.com/products/n8n/reviews/start\">review on G2</a>.",
|
||||
"prompts.npsSurvey.thanks": "Thanks for your feedback",
|
||||
"resourceLocator.id.placeholder": "Enter ID...",
|
||||
"resourceLocator.mode.id": "By ID",
|
||||
"resourceLocator.mode.url": "By URL",
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ITelemetrySettings, ITelemetryTrackProperties, IDataObject } from
|
||||
import type { RouteLocation } from 'vue-router';
|
||||
|
||||
import type { INodeCreateElement, IUpdateInformation } from '@/Interface';
|
||||
import type { IUserNodesPanelSession } from './telemetry.types';
|
||||
import type { IUserNodesPanelSession, RudderStack } from './telemetry.types';
|
||||
import {
|
||||
APPEND_ATTRIBUTION_DEFAULT_PATH,
|
||||
MICROSOFT_TEAMS_NODE_TYPE,
|
||||
@@ -22,7 +22,7 @@ export class Telemetry {
|
||||
|
||||
private previousPath: string;
|
||||
|
||||
private get rudderStack() {
|
||||
private get rudderStack(): RudderStack | undefined {
|
||||
return window.rudderanalytics;
|
||||
}
|
||||
|
||||
@@ -92,12 +92,12 @@ export class Telemetry {
|
||||
traits.user_cloud_id = settingsStore.settings?.n8nMetadata?.userId ?? '';
|
||||
}
|
||||
if (userId) {
|
||||
this.rudderStack.identify(
|
||||
this.rudderStack?.identify(
|
||||
`${instanceId}#${userId}${projectId ? '#' + projectId : ''}`,
|
||||
traits,
|
||||
);
|
||||
} else {
|
||||
this.rudderStack.reset();
|
||||
this.rudderStack?.reset();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +282,9 @@ export class Telemetry {
|
||||
|
||||
private initRudderStack(key: string, url: string, options: IDataObject) {
|
||||
window.rudderanalytics = window.rudderanalytics || [];
|
||||
if (!this.rudderStack) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rudderStack.methods = [
|
||||
'load',
|
||||
@@ -298,6 +301,10 @@ export class Telemetry {
|
||||
|
||||
this.rudderStack.factory = (method: string) => {
|
||||
return (...args: unknown[]) => {
|
||||
if (!this.rudderStack) {
|
||||
throw new Error('RudderStack not initialized');
|
||||
}
|
||||
|
||||
const argsCopy = [method, ...args];
|
||||
this.rudderStack.push(argsCopy);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ interface IUserNodesPanelSessionData {
|
||||
* Simplified version of:
|
||||
* https://github.com/rudderlabs/rudder-sdk-js/blob/master/dist/rudder-sdk-js/index.d.ts
|
||||
*/
|
||||
interface RudderStack extends Array<unknown> {
|
||||
export interface RudderStack extends Array<unknown> {
|
||||
[key: string]: unknown;
|
||||
|
||||
methods: string[];
|
||||
|
||||
303
packages/editor-ui/src/stores/npsStore.store.spec.ts
Normal file
303
packages/editor-ui/src/stores/npsStore.store.spec.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import { useNpsSurveyStore } from './npsSurvey.store';
|
||||
import { THREE_DAYS_IN_MILLIS, TIME, NPS_SURVEY_MODAL_KEY } from '@/constants';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
|
||||
const { openModal, updateNpsSurveyState } = vi.hoisted(() => {
|
||||
return {
|
||||
openModal: vi.fn(),
|
||||
updateNpsSurveyState: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@/stores/ui.store', () => ({
|
||||
useUIStore: vi.fn(() => ({
|
||||
openModal,
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/npsSurvey', () => ({
|
||||
updateNpsSurveyState,
|
||||
}));
|
||||
|
||||
const NOW = 1717602004819;
|
||||
|
||||
vi.useFakeTimers({
|
||||
now: NOW,
|
||||
});
|
||||
|
||||
describe('useNpsSurvey', () => {
|
||||
let npsSurveyStore: ReturnType<typeof useNpsSurveyStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
setActivePinia(createPinia());
|
||||
useSettingsStore().settings.telemetry = { enabled: true };
|
||||
npsSurveyStore = useNpsSurveyStore();
|
||||
});
|
||||
|
||||
it('by default, without login, does not show survey', async () => {
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not show nps survey if user activated less than 3 days ago', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - THREE_DAYS_IN_MILLIS + 10000,
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows nps survey if user activated more than 3 days ago and has yet to see survey', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - THREE_DAYS_IN_MILLIS - 10000,
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith(NPS_SURVEY_MODAL_KEY);
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
ignoredCount: 0,
|
||||
lastShownAt: NOW,
|
||||
waitingForResponse: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show nps survey if user has seen and responded to survey less than 6 months ago', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 10 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
responded: true,
|
||||
lastShownAt: NOW - 2 * TIME.DAY,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('does not show nps survey if user has responded survey more than 7 days ago', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 10 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
responded: true,
|
||||
lastShownAt: NOW - 8 * TIME.DAY,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows nps survey if user has responded survey more than 6 months ago', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 30 * 7 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
responded: true,
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith(NPS_SURVEY_MODAL_KEY);
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
ignoredCount: 0,
|
||||
lastShownAt: NOW,
|
||||
waitingForResponse: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show nps survey if user has ignored survey less than 7 days ago', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 10 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
waitingForResponse: true,
|
||||
lastShownAt: NOW - 5 * TIME.DAY,
|
||||
ignoredCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('shows nps survey if user has ignored survey more than 7 days ago', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 10 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
waitingForResponse: true,
|
||||
lastShownAt: NOW - 8 * TIME.DAY,
|
||||
ignoredCount: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith(NPS_SURVEY_MODAL_KEY);
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
ignoredCount: 0,
|
||||
lastShownAt: NOW,
|
||||
waitingForResponse: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('increments ignore count when survey is ignored', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 30 * 7 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
responded: true,
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.ignoreNpsSurvey();
|
||||
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
ignoredCount: 1,
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
waitingForResponse: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('updates state to responded if ignored more than maximum times', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 30 * 7 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
waitingForResponse: true,
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
ignoredCount: 2,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.ignoreNpsSurvey();
|
||||
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
responded: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('updates state to responded when response is given', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - 30 * 7 * TIME.DAY,
|
||||
npsSurvey: {
|
||||
responded: true,
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
},
|
||||
});
|
||||
|
||||
await npsSurveyStore.respondNpsSurvey();
|
||||
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
responded: true,
|
||||
lastShownAt: NOW - (30 * 6 + 1) * TIME.DAY,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('does not show nps survey twice in the same session', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - THREE_DAYS_IN_MILLIS - 10000,
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).toHaveBeenCalledWith(NPS_SURVEY_MODAL_KEY);
|
||||
expect(updateNpsSurveyState).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
baseUrl: '/rest',
|
||||
}),
|
||||
{
|
||||
ignoredCount: 0,
|
||||
lastShownAt: NOW,
|
||||
waitingForResponse: true,
|
||||
},
|
||||
);
|
||||
|
||||
openModal.mockReset();
|
||||
updateNpsSurveyState.mockReset();
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('resets on logout, preventing nps survey from showing', async () => {
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - THREE_DAYS_IN_MILLIS - 10000,
|
||||
});
|
||||
|
||||
npsSurveyStore.resetNpsSurveyOnLogOut();
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('if telemetry is disabled, does not show nps survey', async () => {
|
||||
useSettingsStore().settings.telemetry = { enabled: false };
|
||||
npsSurveyStore.setupNpsSurveyOnLogin('1', {
|
||||
userActivated: true,
|
||||
userActivatedAt: NOW - THREE_DAYS_IN_MILLIS - 10000,
|
||||
});
|
||||
|
||||
await npsSurveyStore.showNpsSurveyIfPossible();
|
||||
|
||||
expect(openModal).not.toHaveBeenCalled();
|
||||
expect(updateNpsSurveyState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
166
packages/editor-ui/src/stores/npsSurvey.store.ts
Normal file
166
packages/editor-ui/src/stores/npsSurvey.store.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { ref } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useUIStore } from './ui.store';
|
||||
import {
|
||||
SEVEN_DAYS_IN_MILLIS,
|
||||
SIX_MONTHS_IN_MILLIS,
|
||||
THREE_DAYS_IN_MILLIS,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
} from '@/constants';
|
||||
import { useRootStore } from './n8nRoot.store';
|
||||
import type { IUserSettings, NpsSurveyState } from 'n8n-workflow';
|
||||
import { useSettingsStore } from './settings.store';
|
||||
import { updateNpsSurveyState } from '@/api/npsSurvey';
|
||||
import type { IN8nPrompts } from '@/Interface';
|
||||
import { getPromptsData } from '@/api/settings';
|
||||
import { assert } from '@/utils/assert';
|
||||
|
||||
export const MAXIMUM_TIMES_TO_SHOW_SURVEY_IF_IGNORED = 3;
|
||||
|
||||
export const useNpsSurveyStore = defineStore('npsSurvey', () => {
|
||||
const rootStore = useRootStore();
|
||||
const uiStore = useUIStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
|
||||
const shouldShowNpsSurveyNext = ref<boolean>(false);
|
||||
const currentSurveyState = ref<NpsSurveyState | undefined>();
|
||||
const currentUserId = ref<string | undefined>();
|
||||
const promptsData = ref<IN8nPrompts | undefined>();
|
||||
|
||||
function setupNpsSurveyOnLogin(userId: string, settings?: IUserSettings): void {
|
||||
currentUserId.value = userId;
|
||||
|
||||
if (settings) {
|
||||
setShouldShowNpsSurvey(settings);
|
||||
}
|
||||
}
|
||||
|
||||
function setShouldShowNpsSurvey(settings: IUserSettings) {
|
||||
if (!settingsStore.isTelemetryEnabled) {
|
||||
shouldShowNpsSurveyNext.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
currentSurveyState.value = settings.npsSurvey;
|
||||
const userActivated = Boolean(settings.userActivated);
|
||||
const userActivatedAt = settings.userActivatedAt;
|
||||
const lastShownAt = currentSurveyState.value?.lastShownAt;
|
||||
|
||||
if (!userActivated || !userActivatedAt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceActivation = Date.now() - userActivatedAt;
|
||||
if (timeSinceActivation < THREE_DAYS_IN_MILLIS) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSurveyState.value || !lastShownAt) {
|
||||
// user has activated but never seen the nps survey
|
||||
shouldShowNpsSurveyNext.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const timeSinceLastShown = Date.now() - lastShownAt;
|
||||
if ('responded' in currentSurveyState.value && timeSinceLastShown < SIX_MONTHS_IN_MILLIS) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
'waitingForResponse' in currentSurveyState.value &&
|
||||
timeSinceLastShown < SEVEN_DAYS_IN_MILLIS
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
shouldShowNpsSurveyNext.value = true;
|
||||
}
|
||||
|
||||
function resetNpsSurveyOnLogOut() {
|
||||
shouldShowNpsSurveyNext.value = false;
|
||||
}
|
||||
|
||||
async function showNpsSurveyIfPossible() {
|
||||
if (!shouldShowNpsSurveyNext.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
uiStore.openModal(NPS_SURVEY_MODAL_KEY);
|
||||
shouldShowNpsSurveyNext.value = false;
|
||||
|
||||
const updatedState: NpsSurveyState = {
|
||||
waitingForResponse: true,
|
||||
lastShownAt: Date.now(),
|
||||
ignoredCount:
|
||||
currentSurveyState.value && 'ignoredCount' in currentSurveyState.value
|
||||
? currentSurveyState.value.ignoredCount
|
||||
: 0,
|
||||
};
|
||||
await updateNpsSurveyState(rootStore.getRestApiContext, updatedState);
|
||||
currentSurveyState.value = updatedState;
|
||||
}
|
||||
|
||||
async function respondNpsSurvey() {
|
||||
assert(currentSurveyState.value);
|
||||
|
||||
const updatedState: NpsSurveyState = {
|
||||
responded: true,
|
||||
lastShownAt: currentSurveyState.value.lastShownAt,
|
||||
};
|
||||
await updateNpsSurveyState(rootStore.getRestApiContext, updatedState);
|
||||
currentSurveyState.value = updatedState;
|
||||
}
|
||||
|
||||
async function ignoreNpsSurvey() {
|
||||
assert(currentSurveyState.value);
|
||||
|
||||
const state = currentSurveyState.value;
|
||||
const ignoredCount = 'ignoredCount' in state ? state.ignoredCount : 0;
|
||||
|
||||
if (ignoredCount + 1 >= MAXIMUM_TIMES_TO_SHOW_SURVEY_IF_IGNORED) {
|
||||
await respondNpsSurvey();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedState: NpsSurveyState = {
|
||||
waitingForResponse: true,
|
||||
lastShownAt: currentSurveyState.value.lastShownAt,
|
||||
ignoredCount: ignoredCount + 1,
|
||||
};
|
||||
await updateNpsSurveyState(rootStore.getRestApiContext, updatedState);
|
||||
currentSurveyState.value = updatedState;
|
||||
}
|
||||
|
||||
async function fetchPromptsData(): Promise<void> {
|
||||
assert(currentUserId.value);
|
||||
if (!settingsStore.isTelemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
promptsData.value = await getPromptsData(
|
||||
settingsStore.settings.instanceId,
|
||||
currentUserId.value,
|
||||
);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch prompts data');
|
||||
}
|
||||
|
||||
if (promptsData.value?.showContactPrompt) {
|
||||
uiStore.openModal(CONTACT_PROMPT_MODAL_KEY);
|
||||
} else {
|
||||
await useNpsSurveyStore().showNpsSurveyIfPossible();
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
promptsData,
|
||||
resetNpsSurveyOnLogOut,
|
||||
showNpsSurveyIfPossible,
|
||||
ignoreNpsSurvey,
|
||||
respondNpsSurvey,
|
||||
setupNpsSurveyOnLogin,
|
||||
fetchPromptsData,
|
||||
};
|
||||
});
|
||||
@@ -6,22 +6,15 @@ import {
|
||||
testLdapConnection,
|
||||
updateLdapConfig,
|
||||
} from '@/api/ldap';
|
||||
import { getPromptsData, getSettings, submitContactInfo, submitValueSurvey } from '@/api/settings';
|
||||
import { getSettings, submitContactInfo } from '@/api/settings';
|
||||
import { testHealthEndpoint } from '@/api/templates';
|
||||
import type { EnterpriseEditionFeatureValue } from '@/Interface';
|
||||
import {
|
||||
CONTACT_PROMPT_MODAL_KEY,
|
||||
STORES,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
INSECURE_CONNECTION_WARNING,
|
||||
} from '@/constants';
|
||||
import type {
|
||||
EnterpriseEditionFeatureValue,
|
||||
ILdapConfig,
|
||||
IN8nPromptResponse,
|
||||
IN8nPrompts,
|
||||
IN8nValueSurveyData,
|
||||
ISettingsState,
|
||||
} from '@/Interface';
|
||||
import { STORES, INSECURE_CONNECTION_WARNING } from '@/constants';
|
||||
import { UserManagementAuthenticationMethod } from '@/Interface';
|
||||
import type {
|
||||
IDataObject,
|
||||
@@ -45,7 +38,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||
state: (): ISettingsState => ({
|
||||
initialized: false,
|
||||
settings: {} as IN8nUISettings,
|
||||
promptsData: {} as IN8nPrompts,
|
||||
userManagement: {
|
||||
quota: -1,
|
||||
showSetupOnFirstLoad: false,
|
||||
@@ -311,32 +303,9 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||
},
|
||||
};
|
||||
},
|
||||
setPromptsData(promptsData: IN8nPrompts): void {
|
||||
this.promptsData = promptsData;
|
||||
},
|
||||
setAllowedModules(allowedModules: { builtIn?: string[]; external?: string[] }): void {
|
||||
this.settings.allowedModules = allowedModules;
|
||||
},
|
||||
async fetchPromptsData(): Promise<void> {
|
||||
if (!this.isTelemetryEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uiStore = useUIStore();
|
||||
const usersStore = useUsersStore();
|
||||
const promptsData: IN8nPrompts = await getPromptsData(
|
||||
this.settings.instanceId,
|
||||
usersStore.currentUserId || '',
|
||||
);
|
||||
|
||||
if (promptsData && promptsData.showContactPrompt) {
|
||||
uiStore.openModal(CONTACT_PROMPT_MODAL_KEY);
|
||||
} else if (promptsData && promptsData.showValueSurvey) {
|
||||
uiStore.openModal(VALUE_SURVEY_MODAL_KEY);
|
||||
}
|
||||
|
||||
this.setPromptsData(promptsData);
|
||||
},
|
||||
async submitContactInfo(email: string): Promise<IN8nPromptResponse | undefined> {
|
||||
try {
|
||||
const usersStore = useUsersStore();
|
||||
@@ -349,18 +318,6 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
||||
return;
|
||||
}
|
||||
},
|
||||
async submitValueSurvey(params: IN8nValueSurveyData): Promise<IN8nPromptResponse | undefined> {
|
||||
try {
|
||||
const usersStore = useUsersStore();
|
||||
return await submitValueSurvey(
|
||||
this.settings.instanceId,
|
||||
usersStore.currentUserId || '',
|
||||
params,
|
||||
);
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
async testTemplatesEndpoint(): Promise<void> {
|
||||
const timeout = new Promise((_, reject) => setTimeout(() => reject(), 2000));
|
||||
await Promise.race([testHealthEndpoint(this.templatesHost), timeout]);
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
STORES,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
VIEWS,
|
||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||
@@ -55,6 +55,7 @@ import type {
|
||||
AppliedThemeOption,
|
||||
NotificationOptions,
|
||||
ModalState,
|
||||
ModalKey,
|
||||
} from '@/Interface';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useRootStore } from '@/stores/n8nRoot.store';
|
||||
@@ -104,7 +105,7 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||
PERSONALIZATION_MODAL_KEY,
|
||||
INVITE_USER_MODAL_KEY,
|
||||
TAGS_MANAGER_MODAL_KEY,
|
||||
VALUE_SURVEY_MODAL_KEY,
|
||||
NPS_SURVEY_MODAL_KEY,
|
||||
VERSIONS_MODAL_KEY,
|
||||
WORKFLOW_LM_CHAT_MODAL_KEY,
|
||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||
@@ -278,19 +279,19 @@ export const useUIStore = defineStore(STORES.UI, {
|
||||
return this.modals[VERSIONS_MODAL_KEY].open;
|
||||
},
|
||||
isModalOpen() {
|
||||
return (name: string) => this.modals[name].open;
|
||||
return (name: ModalKey) => this.modals[name].open;
|
||||
},
|
||||
isModalActive() {
|
||||
return (name: string) => this.modalStack.length > 0 && name === this.modalStack[0];
|
||||
return (name: ModalKey) => this.modalStack.length > 0 && name === this.modalStack[0];
|
||||
},
|
||||
getModalActiveId() {
|
||||
return (name: string) => this.modals[name].activeId;
|
||||
return (name: ModalKey) => this.modals[name].activeId;
|
||||
},
|
||||
getModalMode() {
|
||||
return (name: string) => this.modals[name].mode;
|
||||
return (name: ModalKey) => this.modals[name].mode;
|
||||
},
|
||||
getModalData() {
|
||||
return (name: string) => this.modals[name].data;
|
||||
return (name: ModalKey) => this.modals[name].data;
|
||||
},
|
||||
getFakeDoorByLocation() {
|
||||
return (location: IFakeDoorLocation) =>
|
||||
|
||||
@@ -42,6 +42,7 @@ import { confirmEmail, getCloudUserInfo } from '@/api/cloudPlans';
|
||||
import { useRBACStore } from '@/stores/rbac.store';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import { inviteUsers, acceptInvitation } from '@/api/invitation';
|
||||
import { useNpsSurveyStore } from './npsSurvey.store';
|
||||
|
||||
const isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
|
||||
const isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner;
|
||||
@@ -110,6 +111,7 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||
const defaultScopes: Scope[] = [];
|
||||
useRBACStore().setGlobalScopes(user.globalScopes || defaultScopes);
|
||||
usePostHog().init(user.featureFlags);
|
||||
useNpsSurveyStore().setupNpsSurveyOnLogin(user.id, user.settings);
|
||||
},
|
||||
unsetCurrentUser() {
|
||||
this.currentUserId = null;
|
||||
@@ -185,6 +187,7 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
||||
useCloudPlanStore().reset();
|
||||
usePostHog().reset();
|
||||
useUIStore().clearBannerStack();
|
||||
useNpsSurveyStore().resetNpsSurveyOnLogOut();
|
||||
},
|
||||
async createOwner(params: {
|
||||
firstName: string;
|
||||
|
||||
@@ -403,6 +403,7 @@ import { useAIStore } from '@/stores/ai.store';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
import { isJSPlumbEndpointElement, isJSPlumbConnection } from '@/utils/typeGuards';
|
||||
import { usePostHog } from '@/stores/posthog.store';
|
||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||
|
||||
interface AddNodeOptions {
|
||||
position?: XYPosition;
|
||||
@@ -464,7 +465,7 @@ export default defineComponent({
|
||||
this.workflowsStore.setWorkflowId(PLACEHOLDER_EMPTY_WORKFLOW_ID);
|
||||
const saved = await this.workflowHelpers.saveCurrentWorkflow({}, false);
|
||||
if (saved) {
|
||||
await this.settingsStore.fetchPromptsData();
|
||||
await this.npsSurveyStore.fetchPromptsData();
|
||||
}
|
||||
this.uiStore.stateIsDirty = false;
|
||||
|
||||
@@ -605,6 +606,7 @@ export default defineComponent({
|
||||
useExecutionsStore,
|
||||
useProjectsStore,
|
||||
useAIStore,
|
||||
useNpsSurveyStore,
|
||||
),
|
||||
nativelyNumberSuffixedDefaults(): string[] {
|
||||
return this.nodeTypesStore.nativelyNumberSuffixedDefaults;
|
||||
@@ -1235,7 +1237,7 @@ export default defineComponent({
|
||||
async onSaveKeyboardShortcut(e: KeyboardEvent) {
|
||||
let saved = await this.workflowHelpers.saveCurrentWorkflow();
|
||||
if (saved) {
|
||||
await this.settingsStore.fetchPromptsData();
|
||||
await this.npsSurveyStore.fetchPromptsData();
|
||||
|
||||
if (this.$route.name === VIEWS.EXECUTION_DEBUG) {
|
||||
await this.$router.replace({
|
||||
@@ -3796,7 +3798,7 @@ export default defineComponent({
|
||||
);
|
||||
if (confirmModal === MODAL_CONFIRM) {
|
||||
const saved = await this.workflowHelpers.saveCurrentWorkflow();
|
||||
if (saved) await this.settingsStore.fetchPromptsData();
|
||||
if (saved) await this.npsSurveyStore.fetchPromptsData();
|
||||
} else if (confirmModal === MODAL_CANCEL) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user