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:
Mutasem Aldmour
2024-06-11 10:23:30 +02:00
committed by GitHub
parent aaa78435b0
commit 50bd5b9080
58 changed files with 1416 additions and 497 deletions

View File

@@ -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;
}

View File

@@ -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,

View 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);
}

View File

@@ -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,

View File

@@ -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 'Youre 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.';

View File

@@ -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({

View File

@@ -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,

View File

@@ -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: {

View File

@@ -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,

View 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>

View File

@@ -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 youd 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>

View File

@@ -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();
}
},
},
});

View File

@@ -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;

View File

@@ -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();
}
}
};

View File

@@ -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
*/

View File

@@ -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 youd 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",

View File

@@ -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);

View File

@@ -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[];

View 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();
});
});

View 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,
};
});

View File

@@ -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]);

View File

@@ -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) =>

View File

@@ -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;

View File

@@ -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;
}