mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(editor): Add user activation survey (#5677)
* ⚡ Add user activation survey
* Fix typo
* Avoid showing the modal when there is a modal view
* Allow to redirect to specific execution
* Improve structure
* Handle errors when sharing feedback
* update withFeatureFlag function
* Fix linting issue
* Set user activation flag on workflowExecutionCompleted event
* Revert update user settings functionality
* Remove unnecessary changes
* fix linting issue
* account for new functionality in tests
* Small improvements
* keep once instace of the model open between tabs
* Add sorting to GET /executions
* type parameters for GET /executions
a
* Add constant for local store key
* Add execution mode filtering
* fix linting issue
* Do not override settings when setting isOnboarded true
* Add update user settings endpoint
* improvements
* revert changes to /GET executions
* Fix typo
* Add userActivated flag to user store
* Add E2E test
* Fix linting issue
* Update pnpm-lock
* Revert unnecessary change
* Centralize user's settings update
* Remove unused ref in userActivationSurvey modal
* Use aliased imports
* Use createEventBus function in component
* Fix tests
This commit is contained in:
61
cypress/e2e/22-user-activation-modal.cy.ts
Normal file
61
cypress/e2e/22-user-activation-modal.cy.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { WorkflowPage, NDV, MainSidebar, UserActivationSurveyModal } from '../pages';
|
||||||
|
import SettingsWithActivationModalEnabled from '../fixtures/Settings_user_activation_modal_enabled.json';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
const mainSidebar = new MainSidebar();
|
||||||
|
const userActivationSurveyModal = new UserActivationSurveyModal();
|
||||||
|
|
||||||
|
const BASE_WEBHOOK_URL = 'http://localhost:5678/webhook';
|
||||||
|
|
||||||
|
describe('User activation survey', () => {
|
||||||
|
it('Should show activation survey', () => {
|
||||||
|
|
||||||
|
cy.resetAll();
|
||||||
|
|
||||||
|
cy.skipSetup();
|
||||||
|
|
||||||
|
cy.intercept('GET', '/rest/settings',(req) => {
|
||||||
|
req.reply(SettingsWithActivationModalEnabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
const path = uuid();
|
||||||
|
const method = 'GET';
|
||||||
|
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas('Webhook');
|
||||||
|
workflowPage.actions.openNode('Webhook');
|
||||||
|
|
||||||
|
//input http method
|
||||||
|
cy.getByTestId('parameter-input-httpMethod').click();
|
||||||
|
cy.getByTestId('parameter-input-httpMethod')
|
||||||
|
.find('.el-select-dropdown')
|
||||||
|
.find('.option-headline')
|
||||||
|
.contains(method)
|
||||||
|
.click();
|
||||||
|
|
||||||
|
//input path method
|
||||||
|
cy.getByTestId('parameter-input-path')
|
||||||
|
.find('.parameter-input')
|
||||||
|
.find('input')
|
||||||
|
.clear()
|
||||||
|
.type(path);
|
||||||
|
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
workflowPage.actions.saveWorkflowOnButtonClick();
|
||||||
|
|
||||||
|
workflowPage.actions.activateWorkflow();
|
||||||
|
|
||||||
|
cy.request(method, `${BASE_WEBHOOK_URL}/${path}`).then((response) => {
|
||||||
|
expect(response.status).to.eq(200);
|
||||||
|
cy.visit('/');
|
||||||
|
cy.reload();
|
||||||
|
mainSidebar.actions.goToCredentials();
|
||||||
|
|
||||||
|
userActivationSurveyModal.getters.modalContainer().should('be.visible');
|
||||||
|
userActivationSurveyModal.getters.feedbackInput().type('testing');
|
||||||
|
userActivationSurveyModal.getters.sendFeedbackButton().click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
90
cypress/fixtures/Settings_user_activation_modal_enabled.json
Normal file
90
cypress/fixtures/Settings_user_activation_modal_enabled.json
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"endpointWebhook": "webhook",
|
||||||
|
"endpointWebhookTest": "webhook-test",
|
||||||
|
"saveDataErrorExecution": "all",
|
||||||
|
"saveDataSuccessExecution": "all",
|
||||||
|
"saveManualExecutions": false,
|
||||||
|
"executionTimeout": -1,
|
||||||
|
"maxExecutionTimeout": 3600,
|
||||||
|
"workflowCallerPolicyDefaultOption": "workflowsFromSameOwner",
|
||||||
|
"timezone": "America/New_York",
|
||||||
|
"urlBaseWebhook": "http://localhost:5678/",
|
||||||
|
"urlBaseEditor": "http://localhost:5678",
|
||||||
|
"versionCli": "0.221.2",
|
||||||
|
"oauthCallbackUrls": {
|
||||||
|
"oauth1": "http://localhost:5678/rest/oauth1-credential/callback",
|
||||||
|
"oauth2": "http://localhost:5678/rest/oauth2-credential/callback"
|
||||||
|
},
|
||||||
|
"versionNotifications": {
|
||||||
|
"enabled": true,
|
||||||
|
"endpoint": "https://api.n8n.io/api/versions/",
|
||||||
|
"infoUrl": "https://docs.n8n.io/getting-started/installation/updating.html"
|
||||||
|
},
|
||||||
|
"instanceId": "c229842c6d1e217486d04caf7223758e08385156ca87a58286c850760c7161f4",
|
||||||
|
"telemetry": {
|
||||||
|
"enabled": true
|
||||||
|
},
|
||||||
|
"posthog": {
|
||||||
|
"enabled": false,
|
||||||
|
"apiHost": "https://ph.n8n.io",
|
||||||
|
"apiKey": "phc_4URIAm1uYfJO7j8kWSe0J8lc8IqnstRLS7Jx8NcakHo",
|
||||||
|
"autocapture": false,
|
||||||
|
"disableSessionRecording": true,
|
||||||
|
"debug": false
|
||||||
|
},
|
||||||
|
"personalizationSurveyEnabled": false,
|
||||||
|
"userActivationSurveyEnabled": true,
|
||||||
|
"defaultLocale": "en",
|
||||||
|
"userManagement": {
|
||||||
|
"enabled": true,
|
||||||
|
"showSetupOnFirstLoad": false,
|
||||||
|
"smtpSetup": false
|
||||||
|
},
|
||||||
|
"sso": {
|
||||||
|
"saml": {
|
||||||
|
"loginEnabled": false,
|
||||||
|
"loginLabel": ""
|
||||||
|
},
|
||||||
|
"ldap": {
|
||||||
|
"loginEnabled": false,
|
||||||
|
"loginLabel": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"publicApi": {
|
||||||
|
"enabled": false,
|
||||||
|
"latestVersion": 1,
|
||||||
|
"path": "api",
|
||||||
|
"swaggerUi": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"workflowTagsDisabled": false,
|
||||||
|
"logLevel": "info",
|
||||||
|
"hiringBannerEnabled": true,
|
||||||
|
"templates": {
|
||||||
|
"enabled": true,
|
||||||
|
"host": "https://api.n8n.io/api/"
|
||||||
|
},
|
||||||
|
"onboardingCallPromptEnabled": true,
|
||||||
|
"executionMode": "regular",
|
||||||
|
"pushBackend": "sse",
|
||||||
|
"communityNodesEnabled": true,
|
||||||
|
"deployment": {
|
||||||
|
"type": "default"
|
||||||
|
},
|
||||||
|
"isNpmAvailable": false,
|
||||||
|
"allowedModules": {},
|
||||||
|
"enterprise": {
|
||||||
|
"sharing": true,
|
||||||
|
"ldap": true,
|
||||||
|
"saml": false,
|
||||||
|
"logStreaming": false,
|
||||||
|
"advancedExecutionFilters": false
|
||||||
|
},
|
||||||
|
"hideUsagePage": false,
|
||||||
|
"license": {
|
||||||
|
"environment": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './credentials-modal';
|
export * from './credentials-modal';
|
||||||
export * from './message-box';
|
export * from './message-box';
|
||||||
export * from './workflow-sharing-modal';
|
export * from './workflow-sharing-modal';
|
||||||
|
export * from './user-activation-survey-modal';
|
||||||
|
|
||||||
|
|||||||
9
cypress/pages/modals/user-activation-survey-modal.ts
Normal file
9
cypress/pages/modals/user-activation-survey-modal.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { BasePage } from './../base';
|
||||||
|
|
||||||
|
export class UserActivationSurveyModal extends BasePage {
|
||||||
|
getters = {
|
||||||
|
modalContainer: () => cy.getByTestId('userActivationSurvey-modal').last(),
|
||||||
|
feedbackInput: () => cy.getByTestId('activation-feedback-input').find('textarea'),
|
||||||
|
sendFeedbackButton: () => cy.getByTestId('send-activation-feedback-button'),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -489,6 +489,7 @@ export interface IN8nUISettings {
|
|||||||
debug: boolean;
|
debug: boolean;
|
||||||
};
|
};
|
||||||
personalizationSurveyEnabled: boolean;
|
personalizationSurveyEnabled: boolean;
|
||||||
|
userActivationSurveyEnabled: boolean;
|
||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
userManagement: IUserManagementSettings;
|
userManagement: IUserManagementSettings;
|
||||||
sso: {
|
sso: {
|
||||||
@@ -547,6 +548,9 @@ export interface IPersonalizationSurveyAnswers {
|
|||||||
|
|
||||||
export interface IUserSettings {
|
export interface IUserSettings {
|
||||||
isOnboarded?: boolean;
|
isOnboarded?: boolean;
|
||||||
|
showUserActivationSurvey?: boolean;
|
||||||
|
firstSuccessfulWorkflowId?: string;
|
||||||
|
userActivated?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserManagementSettings {
|
export interface IUserManagementSettings {
|
||||||
@@ -855,6 +859,7 @@ export interface PublicUser {
|
|||||||
globalRole?: Role;
|
globalRole?: Role;
|
||||||
signInType: AuthProviderType;
|
signInType: AuthProviderType;
|
||||||
disabled: boolean;
|
disabled: boolean;
|
||||||
|
settings?: IUserSettings | null;
|
||||||
inviteAcceptUrl?: string;
|
inviteAcceptUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ class Server extends AbstractServer {
|
|||||||
},
|
},
|
||||||
personalizationSurveyEnabled:
|
personalizationSurveyEnabled:
|
||||||
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
|
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
|
||||||
|
userActivationSurveyEnabled:
|
||||||
|
config.getEnv('userActivationSurvey.enabled') && config.getEnv('diagnostics.enabled'),
|
||||||
defaultLocale: config.getEnv('defaultLocale'),
|
defaultLocale: config.getEnv('defaultLocale'),
|
||||||
userManagement: {
|
userManagement: {
|
||||||
enabled: isUserManagementEnabled(),
|
enabled: isUserManagementEnabled(),
|
||||||
@@ -364,7 +366,6 @@ class Server extends AbstractServer {
|
|||||||
if (config.get('nodes.packagesMissing').length > 0) {
|
if (config.get('nodes.packagesMissing').length > 0) {
|
||||||
this.frontendSettings.missingPackages = true;
|
this.frontendSettings.missingPackages = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.frontendSettings;
|
return this.frontendSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,7 +179,6 @@ export async function withFeatureFlags(
|
|||||||
|
|
||||||
const fetchPromise = new Promise<CurrentUser>(async (resolve) => {
|
const fetchPromise = new Promise<CurrentUser>(async (resolve) => {
|
||||||
user.featureFlags = await postHog.getFeatureFlags(user);
|
user.featureFlags = await postHog.getFeatureFlags(user);
|
||||||
|
|
||||||
resolve(user);
|
resolve(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { whereClause } from '@/UserManagement/UserManagementHelper';
|
|||||||
import omit from 'lodash.omit';
|
import omit from 'lodash.omit';
|
||||||
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
import { PermissionChecker } from './UserManagement/PermissionChecker';
|
||||||
import { isWorkflowIdValid } from './utils';
|
import { isWorkflowIdValid } from './utils';
|
||||||
|
import { UserService } from './user/user.service';
|
||||||
|
|
||||||
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');
|
||||||
|
|
||||||
@@ -429,7 +430,7 @@ export async function isBelowOnboardingThreshold(user: User): Promise<boolean> {
|
|||||||
|
|
||||||
// user is above threshold --> set flag in settings
|
// user is above threshold --> set flag in settings
|
||||||
if (!belowThreshold) {
|
if (!belowThreshold) {
|
||||||
void Db.collections.User.update(user.id, { settings: { isOnboarded: true } });
|
void UserService.updateUserSettings(user.id, { isOnboarded: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
return belowThreshold;
|
return belowThreshold;
|
||||||
|
|||||||
@@ -1042,6 +1042,15 @@ export const schema = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
userActivationSurvey: {
|
||||||
|
enabled: {
|
||||||
|
doc: 'Whether user activation survey is enabled.',
|
||||||
|
format: Boolean,
|
||||||
|
default: true,
|
||||||
|
env: 'N8N_USER_ACTIVATION_SURVEY_ENABLED',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
diagnostics: {
|
diagnostics: {
|
||||||
enabled: {
|
enabled: {
|
||||||
doc: 'Whether diagnostic mode is enabled.',
|
doc: 'Whether diagnostic mode is enabled.',
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ import { issueCookie } from '@/auth/jwt';
|
|||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import type { Repository } from 'typeorm';
|
import type { Repository } from 'typeorm';
|
||||||
import type { ILogger } from 'n8n-workflow';
|
import type { ILogger } from 'n8n-workflow';
|
||||||
import { AuthenticatedRequest, MeRequest, UserUpdatePayload } from '@/requests';
|
import {
|
||||||
|
AuthenticatedRequest,
|
||||||
|
MeRequest,
|
||||||
|
UserSettingsUpdatePayload,
|
||||||
|
UserUpdatePayload,
|
||||||
|
} from '@/requests';
|
||||||
import type {
|
import type {
|
||||||
PublicUser,
|
PublicUser,
|
||||||
IDatabaseCollections,
|
IDatabaseCollections,
|
||||||
@@ -23,6 +28,7 @@ import type {
|
|||||||
} from '@/Interfaces';
|
} from '@/Interfaces';
|
||||||
import { randomBytes } from 'crypto';
|
import { randomBytes } from 'crypto';
|
||||||
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
|
||||||
|
import { UserService } from '@/user/user.service';
|
||||||
|
|
||||||
@RestController('/me')
|
@RestController('/me')
|
||||||
export class MeController {
|
export class MeController {
|
||||||
@@ -52,7 +58,7 @@ export class MeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the logged-in user's settings, except password.
|
* Update the logged-in user's properties, except password.
|
||||||
*/
|
*/
|
||||||
@Patch('/')
|
@Patch('/')
|
||||||
async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> {
|
async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> {
|
||||||
@@ -234,4 +240,22 @@ export class MeController {
|
|||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the logged-in user's settings.
|
||||||
|
*/
|
||||||
|
@Patch('/settings')
|
||||||
|
async updateCurrentUserSettings(req: MeRequest.UserSettingsUpdate): Promise<User['settings']> {
|
||||||
|
const payload = plainToInstance(UserSettingsUpdatePayload, req.body);
|
||||||
|
const { id } = req.user;
|
||||||
|
|
||||||
|
await UserService.updateUserSettings(id, payload);
|
||||||
|
|
||||||
|
const user = await this.userRepository.findOneOrFail({
|
||||||
|
select: ['settings'],
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
|
||||||
|
return user.settings;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { QueryFailedError } from 'typeorm';
|
|||||||
import { Container } from 'typedi';
|
import { Container } from 'typedi';
|
||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { UserService } from '@/user/user.service';
|
||||||
|
|
||||||
enum StatisticsUpsertResult {
|
enum StatisticsUpsertResult {
|
||||||
insert = 'insert',
|
insert = 'insert',
|
||||||
@@ -112,6 +113,15 @@ export async function workflowExecutionCompleted(
|
|||||||
user_id: owner.id,
|
user_id: owner.id,
|
||||||
workflow_id: workflowId,
|
workflow_id: workflowId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!owner.settings?.firstSuccessfulWorkflowId) {
|
||||||
|
await UserService.updateUserSettings(owner.id, {
|
||||||
|
firstSuccessfulWorkflowId: workflowId,
|
||||||
|
userActivated: true,
|
||||||
|
showUserActivationSurvey: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Send the metrics
|
// Send the metrics
|
||||||
await Container.get(InternalHooks).onFirstProductionWorkflowSuccess(metrics);
|
await Container.get(InternalHooks).onFirstProductionWorkflowSuccess(metrics);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export class ExecutionsService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Omit `data` from the Execution since it is the largest and not necesary for the list.
|
// Omit `data` from the Execution since it is the largest and not necessary for the list.
|
||||||
let query = Db.collections.Execution.createQueryBuilder('execution')
|
let query = Db.collections.Execution.createQueryBuilder('execution')
|
||||||
.select([
|
.select([
|
||||||
'execution.id',
|
'execution.id',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import type {
|
|||||||
IWorkflowSettings,
|
IWorkflowSettings,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { IsEmail, IsString, Length } from 'class-validator';
|
import { IsBoolean, IsEmail, IsOptional, IsString, Length } from 'class-validator';
|
||||||
import { NoXss } from '@db/utils/customValidators';
|
import { NoXss } from '@db/utils/customValidators';
|
||||||
import type { PublicUser, IExecutionDeleteFilter, IWorkflowDb } from '@/Interfaces';
|
import type { PublicUser, IExecutionDeleteFilter, IWorkflowDb } from '@/Interfaces';
|
||||||
import type { Role } from '@db/entities/Role';
|
import type { Role } from '@db/entities/Role';
|
||||||
@@ -31,6 +31,15 @@ export class UserUpdatePayload implements Pick<User, 'email' | 'firstName' | 'la
|
|||||||
@Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' })
|
@Length(1, 32, { message: 'Last name must be $constraint1 to $constraint2 characters long.' })
|
||||||
lastName: string;
|
lastName: string;
|
||||||
}
|
}
|
||||||
|
export class UserSettingsUpdatePayload {
|
||||||
|
@IsBoolean({ message: 'showUserActivationSurvey should be a boolean' })
|
||||||
|
@IsOptional()
|
||||||
|
showUserActivationSurvey: boolean;
|
||||||
|
|
||||||
|
@IsBoolean({ message: 'userActivated should be a boolean' })
|
||||||
|
@IsOptional()
|
||||||
|
userActivated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export type AuthlessRequest<
|
export type AuthlessRequest<
|
||||||
RouteParams = {},
|
RouteParams = {},
|
||||||
@@ -161,6 +170,7 @@ export declare namespace ExecutionRequest {
|
|||||||
// ----------------------------------
|
// ----------------------------------
|
||||||
|
|
||||||
export declare namespace MeRequest {
|
export declare namespace MeRequest {
|
||||||
|
export type UserSettingsUpdate = AuthenticatedRequest<{}, {}, UserSettingsUpdatePayload>;
|
||||||
export type UserUpdate = AuthenticatedRequest<{}, {}, UserUpdatePayload>;
|
export type UserUpdate = AuthenticatedRequest<{}, {}, UserUpdatePayload>;
|
||||||
export type Password = AuthenticatedRequest<
|
export type Password = AuthenticatedRequest<
|
||||||
{},
|
{},
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { EntityManager, FindOptionsWhere } from 'typeorm';
|
|||||||
import { In } from 'typeorm';
|
import { In } from 'typeorm';
|
||||||
import * as Db from '@/Db';
|
import * as Db from '@/Db';
|
||||||
import { User } from '@db/entities/User';
|
import { User } from '@db/entities/User';
|
||||||
|
import type { IUserSettings } from '@/Interfaces';
|
||||||
|
|
||||||
export class UserService {
|
export class UserService {
|
||||||
static async get(where: FindOptionsWhere<User>): Promise<User | null> {
|
static async get(where: FindOptionsWhere<User>): Promise<User | null> {
|
||||||
@@ -14,4 +15,11 @@ export class UserService {
|
|||||||
static async getByIds(transaction: EntityManager, ids: string[]) {
|
static async getByIds(transaction: EntityManager, ids: string[]) {
|
||||||
return transaction.find(User, { where: { id: In(ids) } });
|
return transaction.find(User, { where: { id: In(ids) } });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async updateUserSettings(id: string, userSettings: Partial<IUserSettings>) {
|
||||||
|
const { settings: currentSettings } = await Db.collections.User.findOneOrFail({
|
||||||
|
where: { id },
|
||||||
|
});
|
||||||
|
return Db.collections.User.update(id, { settings: { ...currentSettings, ...userSettings } });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { getLogger } from '@/Logger';
|
|||||||
import { InternalHooks } from '@/InternalHooks';
|
import { InternalHooks } from '@/InternalHooks';
|
||||||
|
|
||||||
import { mockInstance } from '../integration/shared/utils';
|
import { mockInstance } from '../integration/shared/utils';
|
||||||
|
import { UserService } from '@/user/user.service';
|
||||||
|
|
||||||
type WorkflowStatisticsRepository = Repository<WorkflowStatistics>;
|
type WorkflowStatisticsRepository = Repository<WorkflowStatistics>;
|
||||||
jest.mock('@/Db', () => {
|
jest.mock('@/Db', () => {
|
||||||
@@ -24,6 +25,8 @@ jest.mock('@/Db', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.spyOn(UserService, 'updateUserSettings').mockImplementation();
|
||||||
|
|
||||||
describe('Events', () => {
|
describe('Events', () => {
|
||||||
const dbType = config.getEnv('database.type');
|
const dbType = config.getEnv('database.type');
|
||||||
const fakeUser = Object.assign(new User(), { id: 'abcde-fghij' });
|
const fakeUser = Object.assign(new User(), { id: 'abcde-fghij' });
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
"@jsplumb/core": "^5.13.2",
|
"@jsplumb/core": "^5.13.2",
|
||||||
"@jsplumb/util": "^5.13.2",
|
"@jsplumb/util": "^5.13.2",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
|
"canvas-confetti": "^1.6.0",
|
||||||
"codemirror-lang-html-n8n": "^1.0.0",
|
"codemirror-lang-html-n8n": "^1.0.0",
|
||||||
"codemirror-lang-n8n-expression": "^0.2.0",
|
"codemirror-lang-n8n-expression": "^0.2.0",
|
||||||
"dateformat": "^3.0.3",
|
"dateformat": "^3.0.3",
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.16.5",
|
"@testing-library/jest-dom": "^5.16.5",
|
||||||
"@testing-library/user-event": "^14.4.3",
|
"@testing-library/user-event": "^14.4.3",
|
||||||
"@testing-library/vue": "^5.8.3",
|
"@testing-library/vue": "^5.8.3",
|
||||||
|
"@types/canvas-confetti": "^1.6.0",
|
||||||
"@types/dateformat": "^3.0.0",
|
"@types/dateformat": "^3.0.0",
|
||||||
"@types/express": "^4.17.6",
|
"@types/express": "^4.17.6",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
|
|||||||
@@ -594,6 +594,12 @@ export interface IUserResponse {
|
|||||||
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
personalizationAnswers?: IPersonalizationSurveyVersions | null;
|
||||||
isPending: boolean;
|
isPending: boolean;
|
||||||
signInType?: SignInType;
|
signInType?: SignInType;
|
||||||
|
settings?: {
|
||||||
|
isOnboarded?: boolean;
|
||||||
|
showUserActivationSurvey?: boolean;
|
||||||
|
firstSuccessfulWorkflowId?: string;
|
||||||
|
userActivated?: boolean;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CurrentUserResponse extends IUserResponse {
|
export interface CurrentUserResponse extends IUserResponse {
|
||||||
@@ -749,6 +755,7 @@ export interface IN8nUISettings {
|
|||||||
versionNotifications: IVersionNotificationSettings;
|
versionNotifications: IVersionNotificationSettings;
|
||||||
instanceId: string;
|
instanceId: string;
|
||||||
personalizationSurveyEnabled: boolean;
|
personalizationSurveyEnabled: boolean;
|
||||||
|
userActivationSurveyEnabled: boolean;
|
||||||
telemetry: ITelemetrySettings;
|
telemetry: ITelemetrySettings;
|
||||||
userManagement: IUserManagementConfig;
|
userManagement: IUserManagementConfig;
|
||||||
defaultLocale: string;
|
defaultLocale: string;
|
||||||
|
|||||||
@@ -89,11 +89,23 @@ export async function changePassword(
|
|||||||
|
|
||||||
export function updateCurrentUser(
|
export function updateCurrentUser(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
params: { id: string; firstName: string; lastName: string; email: string },
|
params: {
|
||||||
|
id?: string;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
email: string;
|
||||||
|
},
|
||||||
): Promise<IUserResponse> {
|
): Promise<IUserResponse> {
|
||||||
return makeRestApiRequest(context, 'PATCH', '/me', params as unknown as IDataObject);
|
return makeRestApiRequest(context, 'PATCH', '/me', params as unknown as IDataObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateCurrentUserSettings(
|
||||||
|
context: IRestApiContext,
|
||||||
|
settings: IUserResponse['settings'],
|
||||||
|
): Promise<IUserResponse['settings']> {
|
||||||
|
return makeRestApiRequest(context, 'PATCH', '/me/settings', settings);
|
||||||
|
}
|
||||||
|
|
||||||
export function updateCurrentUserPassword(
|
export function updateCurrentUserPassword(
|
||||||
context: IRestApiContext,
|
context: IRestApiContext,
|
||||||
params: { newPassword: string; currentPassword: string },
|
params: { newPassword: string; currentPassword: string },
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { IRestApiContext } from '@/Interface';
|
import { IExecutionsCurrentSummaryExtended, IRestApiContext } from '@/Interface';
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import {
|
||||||
|
ExecutionFilters,
|
||||||
|
ExecutionOptions,
|
||||||
|
ExecutionStatus,
|
||||||
|
IDataObject,
|
||||||
|
WorkflowExecuteMode,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { makeRestApiRequest } from '@/utils';
|
import { makeRestApiRequest } from '@/utils';
|
||||||
|
|
||||||
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
export async function getNewWorkflow(context: IRestApiContext, name?: string) {
|
||||||
@@ -30,8 +36,12 @@ export async function getCurrentExecutions(context: IRestApiContext, filter: IDa
|
|||||||
return await makeRestApiRequest(context, 'GET', '/executions-current', { filter });
|
return await makeRestApiRequest(context, 'GET', '/executions-current', { filter });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getFinishedExecutions(context: IRestApiContext, filter: IDataObject) {
|
export async function getExecutions(
|
||||||
return await makeRestApiRequest(context, 'GET', '/executions', { filter });
|
context: IRestApiContext,
|
||||||
|
filter?: ExecutionFilters,
|
||||||
|
options?: ExecutionOptions,
|
||||||
|
): Promise<{ count: number; results: IExecutionsCurrentSummaryExtended[]; estimated: boolean }> {
|
||||||
|
return await makeRestApiRequest(context, 'GET', '/executions', { filter, ...options });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getExecutionData(context: IRestApiContext, executionId: string) {
|
export async function getExecutionData(context: IRestApiContext, executionId: string) {
|
||||||
|
|||||||
@@ -87,6 +87,10 @@
|
|||||||
<ImportCurlModal />
|
<ImportCurlModal />
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="USER_ACTIVATION_SURVEY_MODAL">
|
||||||
|
<WorkflowSuccessModal />
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
<ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY">
|
<ModalRoot :name="COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY">
|
||||||
<template #default="{ modalName, activeId, mode }">
|
<template #default="{ modalName, activeId, mode }">
|
||||||
<CommunityPackageManageConfirmModal
|
<CommunityPackageManageConfirmModal
|
||||||
@@ -134,6 +138,7 @@ import {
|
|||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
IMPORT_CURL_MODAL_KEY,
|
IMPORT_CURL_MODAL_KEY,
|
||||||
LOG_STREAM_MODAL_KEY,
|
LOG_STREAM_MODAL_KEY,
|
||||||
|
USER_ACTIVATION_SURVEY_MODAL,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
import AboutModal from './AboutModal.vue';
|
import AboutModal from './AboutModal.vue';
|
||||||
@@ -157,6 +162,7 @@ import ExecutionsModal from './ExecutionsModal.vue';
|
|||||||
import ActivationModal from './ActivationModal.vue';
|
import ActivationModal from './ActivationModal.vue';
|
||||||
import ImportCurlModal from './ImportCurlModal.vue';
|
import ImportCurlModal from './ImportCurlModal.vue';
|
||||||
import WorkflowShareModal from './WorkflowShareModal.ee.vue';
|
import WorkflowShareModal from './WorkflowShareModal.ee.vue';
|
||||||
|
import WorkflowSuccessModal from './UserActivationSurveyModal.vue';
|
||||||
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
|
import EventDestinationSettingsModal from '@/components/SettingsLogStreaming/EventDestinationSettingsModal.ee.vue';
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
@@ -184,6 +190,7 @@ export default Vue.extend({
|
|||||||
WorkflowShareModal,
|
WorkflowShareModal,
|
||||||
ImportCurlModal,
|
ImportCurlModal,
|
||||||
EventDestinationSettingsModal,
|
EventDestinationSettingsModal,
|
||||||
|
WorkflowSuccessModal,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY,
|
||||||
@@ -207,6 +214,7 @@ export default Vue.extend({
|
|||||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||||
IMPORT_CURL_MODAL_KEY,
|
IMPORT_CURL_MODAL_KEY,
|
||||||
LOG_STREAM_MODAL_KEY,
|
LOG_STREAM_MODAL_KEY,
|
||||||
|
USER_ACTIVATION_SURVEY_MODAL,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
174
packages/editor-ui/src/components/UserActivationSurveyModal.vue
Normal file
174
packages/editor-ui/src/components/UserActivationSurveyModal.vue
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
width="500px"
|
||||||
|
:title="locale.baseText('userActivationSurveyModal.title')"
|
||||||
|
:eventBus="modalBus"
|
||||||
|
:name="USER_ACTIVATION_SURVEY_MODAL"
|
||||||
|
:center="true"
|
||||||
|
:beforeClose="beforeClosingModal"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div :class="$style.container">
|
||||||
|
<div :class="$style.description">
|
||||||
|
<i18n path="userActivationSurveyModal.description.workflowRan">
|
||||||
|
<template #workflow>
|
||||||
|
<n8n-text :bold="true"> {{ workflowName }} </n8n-text>
|
||||||
|
</template>
|
||||||
|
<template #ranSuccessfully>
|
||||||
|
<n8n-text :bold="true" :class="$style.link">
|
||||||
|
{{
|
||||||
|
locale.baseText('userActivationSurveyModal.description.workflowRanSuccessfully')
|
||||||
|
}}
|
||||||
|
</n8n-text>
|
||||||
|
</template>
|
||||||
|
<template #savedTime>
|
||||||
|
<n8n-text>
|
||||||
|
{{ locale.baseText('userActivationSurveyModal.description.savedTime') }}
|
||||||
|
</n8n-text>
|
||||||
|
</template>
|
||||||
|
</i18n>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.form">
|
||||||
|
<n8n-input-label
|
||||||
|
:label="$locale.baseText('userActivationSurveyModal.form.label')"
|
||||||
|
color="text-dark"
|
||||||
|
>
|
||||||
|
<n8n-input
|
||||||
|
type="textarea"
|
||||||
|
:maxlength="FEEDBACK_MAX_LENGTH"
|
||||||
|
:rows="3"
|
||||||
|
v-model="feedback"
|
||||||
|
@input="onInput"
|
||||||
|
data-test-id="activation-feedback-input"
|
||||||
|
/>
|
||||||
|
</n8n-input-label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<div :class="$style.modalFooter">
|
||||||
|
<n8n-button
|
||||||
|
:disabled="!hasAnyChanges"
|
||||||
|
@click="onShareFeedback"
|
||||||
|
size="large"
|
||||||
|
float="right"
|
||||||
|
:label="locale.baseText('userActivationSurveyModal.form.button.shareFeedback')"
|
||||||
|
data-test-id="send-activation-feedback-button"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
import Modal from '@/components/Modal.vue';
|
||||||
|
import { USER_ACTIVATION_SURVEY_MODAL } from '@/constants';
|
||||||
|
import { useUsersStore } from '@/stores/users';
|
||||||
|
|
||||||
|
import confetti from 'canvas-confetti';
|
||||||
|
import { telemetry } from '@/plugins/telemetry';
|
||||||
|
import { i18n as locale } from '@/plugins/i18n';
|
||||||
|
import { Notification } from 'element-ui';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows';
|
||||||
|
import { createEventBus } from '@/event-bus';
|
||||||
|
|
||||||
|
const FEEDBACK_MAX_LENGTH = 300;
|
||||||
|
|
||||||
|
const userStore = useUsersStore();
|
||||||
|
const workflowStore = useWorkflowsStore();
|
||||||
|
|
||||||
|
const hasAnyChanges = ref(false);
|
||||||
|
const feedback = ref('');
|
||||||
|
const modalBus = createEventBus();
|
||||||
|
const workflowName = ref('');
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const currentSettings = getCurrentSettings();
|
||||||
|
try {
|
||||||
|
const { name } = await workflowStore.fetchWorkflow(
|
||||||
|
currentSettings?.firstSuccessfulWorkflowId ?? '',
|
||||||
|
);
|
||||||
|
workflowName.value = name;
|
||||||
|
showConfetti();
|
||||||
|
} catch (e) {}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onShareFeedback = () => {
|
||||||
|
telemetry.track('User responded to activation modal', { response: getFeedback() });
|
||||||
|
showSharedFeedbackSuccess();
|
||||||
|
modalBus.emit('close');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCurrentSettings = () => {
|
||||||
|
return userStore.currentUser?.settings;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getFeedback = () => {
|
||||||
|
return feedback.value.slice(0, FEEDBACK_MAX_LENGTH);
|
||||||
|
};
|
||||||
|
|
||||||
|
const beforeClosingModal = async () => {
|
||||||
|
const currentUser = userStore.currentUser;
|
||||||
|
if (currentUser) {
|
||||||
|
try {
|
||||||
|
await userStore.updateUserSettings({ showUserActivationSurvey: false });
|
||||||
|
} catch {
|
||||||
|
showSharedFeedbackError();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onInput = () => {
|
||||||
|
hasAnyChanges.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSharedFeedbackSuccess = () => {
|
||||||
|
Notification.success({
|
||||||
|
title: locale.baseText('userActivationSurveyModal.sharedFeedback.success'),
|
||||||
|
message: '',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showSharedFeedbackError = () => {
|
||||||
|
Notification.error({
|
||||||
|
title: locale.baseText('userActivationSurveyModal.sharedFeedback.error'),
|
||||||
|
message: '',
|
||||||
|
position: 'bottom-right',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const showConfetti = () => {
|
||||||
|
confetti({
|
||||||
|
particleCount: 200,
|
||||||
|
spread: 100,
|
||||||
|
origin: { y: 0.6 },
|
||||||
|
zIndex: 2050,
|
||||||
|
colors: ['5C4EC2', 'D7E6F1', 'FF9284', '8D7FED', 'B8AFF9', 'FF6D5A'],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.form {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.description > * {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.container > * {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -46,6 +46,7 @@ export const COMMUNITY_PACKAGE_INSTALL_MODAL_KEY = 'communityPackageInstall';
|
|||||||
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
|
export const COMMUNITY_PACKAGE_CONFIRM_MODAL_KEY = 'communityPackageManageConfirm';
|
||||||
export const IMPORT_CURL_MODAL_KEY = 'importCurl';
|
export const IMPORT_CURL_MODAL_KEY = 'importCurl';
|
||||||
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
|
export const LOG_STREAM_MODAL_KEY = 'settingsLogStream';
|
||||||
|
export const USER_ACTIVATION_SURVEY_MODAL = 'userActivationSurvey';
|
||||||
|
|
||||||
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
export const COMMUNITY_PACKAGE_MANAGE_ACTIONS = {
|
||||||
UNINSTALL: 'uninstall',
|
UNINSTALL: 'uninstall',
|
||||||
@@ -325,6 +326,7 @@ export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY
|
|||||||
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
|
||||||
export const LOCAL_STORAGE_MAPPING_IS_ONBOARDED = 'N8N_MAPPING_ONBOARDED';
|
export const LOCAL_STORAGE_MAPPING_IS_ONBOARDED = 'N8N_MAPPING_ONBOARDED';
|
||||||
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
|
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
|
||||||
|
export const LOCAL_STORAGE_ACTIVE_MODAL = 'N8N_ACTIVE_MODAL';
|
||||||
export const LOCAL_STORAGE_THEME = 'N8N_THEME';
|
export const LOCAL_STORAGE_THEME = 'N8N_THEME';
|
||||||
export const LOCAL_STORAGE_EXPERIMENT_OVERRIDES = 'N8N_EXPERIMENT_OVERRIDES';
|
export const LOCAL_STORAGE_EXPERIMENT_OVERRIDES = 'N8N_EXPERIMENT_OVERRIDES';
|
||||||
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
|
export const BASE_NODE_SURVEY_URL = 'https://n8n-community.typeform.com/to/BvmzxqYv#nodename=';
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ import { I18nPlugin, i18nInstance } from './plugins/i18n';
|
|||||||
import { createPinia, PiniaVuePlugin } from 'pinia';
|
import { createPinia, PiniaVuePlugin } from 'pinia';
|
||||||
|
|
||||||
import { useWebhooksStore } from './stores/webhooks';
|
import { useWebhooksStore } from './stores/webhooks';
|
||||||
|
import { useUsersStore } from './stores/users';
|
||||||
|
import { VIEWS } from '@/constants';
|
||||||
|
import { useUIStore } from './stores/ui';
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
@@ -44,6 +47,12 @@ new Vue({
|
|||||||
|
|
||||||
router.afterEach((to, from) => {
|
router.afterEach((to, from) => {
|
||||||
runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
|
runExternalHook('main.routeChange', useWebhooksStore(), { from, to });
|
||||||
|
const userStore = useUsersStore();
|
||||||
|
if (userStore.currentUser && to.name && to.name !== VIEWS.SIGNOUT && !to.name.includes('Modal')) {
|
||||||
|
setTimeout(() => {
|
||||||
|
userStore.showUserActivationSurveyModal();
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!import.meta.env.PROD) {
|
if (!import.meta.env.PROD) {
|
||||||
|
|||||||
@@ -1740,6 +1740,14 @@
|
|||||||
"settings.sso.actionBox.title": "Available on Enterprise plan",
|
"settings.sso.actionBox.title": "Available on Enterprise plan",
|
||||||
"settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.",
|
"settings.sso.actionBox.description": "Use Single Sign On to consolidate authentication into a single platform to improve security and agility.",
|
||||||
"settings.sso.actionBox.buttonText": "See plans",
|
"settings.sso.actionBox.buttonText": "See plans",
|
||||||
|
"userActivationSurveyModal.title": "Congrats! Your workflow ran automatically 👏",
|
||||||
|
"userActivationSurveyModal.description.workflowRan": "{workflow} {ranSuccessfully} {savedTime}",
|
||||||
|
"userActivationSurveyModal.description.workflowRanSuccessfully": "ran successfully",
|
||||||
|
"userActivationSurveyModal.description.savedTime": "for the first time. Looks like n8n just saved you some time! Can you help us make n8n even better and answer the following question?",
|
||||||
|
"userActivationSurveyModal.form.label": "What almost stopped you from creating this workflow?",
|
||||||
|
"userActivationSurveyModal.form.button.shareFeedback": "Share feedback",
|
||||||
|
"userActivationSurveyModal.sharedFeedback.success": "Thanks for your feedback",
|
||||||
|
"userActivationSurveyModal.sharedFeedback.error": "Problem sharing feedback, try again",
|
||||||
"sso.login.divider": "or",
|
"sso.login.divider": "or",
|
||||||
"sso.login.button": "Continue with SSO"
|
"sso.login.button": "Continue with SSO"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,6 +127,13 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
|
|||||||
this.settings.personalizationSurveyEnabled
|
this.settings.personalizationSurveyEnabled
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
isUserActivationSurveyEnabled(): boolean {
|
||||||
|
return (
|
||||||
|
this.settings.telemetry &&
|
||||||
|
this.settings.telemetry.enabled &&
|
||||||
|
this.settings.userActivationSurveyEnabled
|
||||||
|
);
|
||||||
|
},
|
||||||
telemetry(): ITelemetrySettings {
|
telemetry(): ITelemetrySettings {
|
||||||
return this.settings.telemetry;
|
return this.settings.telemetry;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
WORKFLOW_ACTIVE_MODAL_KEY,
|
WORKFLOW_ACTIVE_MODAL_KEY,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
WORKFLOW_SHARE_MODAL_KEY,
|
WORKFLOW_SHARE_MODAL_KEY,
|
||||||
|
USER_ACTIVATION_SURVEY_MODAL,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
import {
|
import {
|
||||||
CurlToJSONResponse,
|
CurlToJSONResponse,
|
||||||
@@ -124,6 +125,9 @@ export const useUIStore = defineStore(STORES.UI, {
|
|||||||
activeId: null,
|
activeId: null,
|
||||||
showAuthSelector: false,
|
showAuthSelector: false,
|
||||||
},
|
},
|
||||||
|
[USER_ACTIVATION_SURVEY_MODAL]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
modalStack: [],
|
modalStack: [],
|
||||||
sidebarMenuCollapsed: true,
|
sidebarMenuCollapsed: true,
|
||||||
|
|||||||
@@ -16,10 +16,16 @@ import {
|
|||||||
submitPersonalizationSurvey,
|
submitPersonalizationSurvey,
|
||||||
updateCurrentUser,
|
updateCurrentUser,
|
||||||
updateCurrentUserPassword,
|
updateCurrentUserPassword,
|
||||||
|
updateCurrentUserSettings,
|
||||||
validatePasswordToken,
|
validatePasswordToken,
|
||||||
validateSignupToken,
|
validateSignupToken,
|
||||||
} from '@/api/users';
|
} from '@/api/users';
|
||||||
import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
|
import {
|
||||||
|
PERSONALIZATION_MODAL_KEY,
|
||||||
|
USER_ACTIVATION_SURVEY_MODAL,
|
||||||
|
STORES,
|
||||||
|
LOCAL_STORAGE_ACTIVE_MODAL,
|
||||||
|
} from '@/constants';
|
||||||
import type {
|
import type {
|
||||||
ICredentialsResponse,
|
ICredentialsResponse,
|
||||||
IInviteResponse,
|
IInviteResponse,
|
||||||
@@ -55,6 +61,9 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
|||||||
allUsers(): IUser[] {
|
allUsers(): IUser[] {
|
||||||
return Object.values(this.users);
|
return Object.values(this.users);
|
||||||
},
|
},
|
||||||
|
userActivated(): boolean {
|
||||||
|
return Boolean(this.currentUser?.settings?.userActivated);
|
||||||
|
},
|
||||||
currentUser(): IUser | null {
|
currentUser(): IUser | null {
|
||||||
return this.currentUserId ? this.users[this.currentUserId] : null;
|
return this.currentUserId ? this.users[this.currentUserId] : null;
|
||||||
},
|
},
|
||||||
@@ -236,6 +245,17 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
|||||||
const user = await updateCurrentUser(rootStore.getRestApiContext, params);
|
const user = await updateCurrentUser(rootStore.getRestApiContext, params);
|
||||||
this.addUsers([user]);
|
this.addUsers([user]);
|
||||||
},
|
},
|
||||||
|
async updateUserSettings(settings: IUserResponse['settings']): Promise<void> {
|
||||||
|
const rootStore = useRootStore();
|
||||||
|
const updatedSettings = await updateCurrentUserSettings(
|
||||||
|
rootStore.getRestApiContext,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
if (this.currentUser) {
|
||||||
|
this.currentUser.settings = updatedSettings;
|
||||||
|
this.addUsers([this.currentUser]);
|
||||||
|
}
|
||||||
|
},
|
||||||
async updateCurrentUserPassword({
|
async updateCurrentUserPassword({
|
||||||
password,
|
password,
|
||||||
currentPassword,
|
currentPassword,
|
||||||
@@ -287,6 +307,16 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
|||||||
uiStore.openModal(PERSONALIZATION_MODAL_KEY);
|
uiStore.openModal(PERSONALIZATION_MODAL_KEY);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
async showUserActivationSurveyModal() {
|
||||||
|
const settingsStore = useSettingsStore();
|
||||||
|
if (settingsStore.isUserActivationSurveyEnabled) {
|
||||||
|
const currentUser = this.currentUser;
|
||||||
|
if (currentUser?.settings?.showUserActivationSurvey) {
|
||||||
|
const uiStore = useUIStore();
|
||||||
|
uiStore.openModal(USER_ACTIVATION_SURVEY_MODAL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
async skipOwnerSetup(): Promise<void> {
|
async skipOwnerSetup(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const rootStore = useRootStore();
|
const rootStore = useRootStore();
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ import {
|
|||||||
getActiveWorkflows,
|
getActiveWorkflows,
|
||||||
getCurrentExecutions,
|
getCurrentExecutions,
|
||||||
getExecutionData,
|
getExecutionData,
|
||||||
getFinishedExecutions,
|
getExecutions,
|
||||||
getNewWorkflow,
|
getNewWorkflow,
|
||||||
getWorkflow,
|
getWorkflow,
|
||||||
getWorkflows,
|
getWorkflows,
|
||||||
@@ -942,7 +942,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
|||||||
requestFilter: ExecutionsQueryFilter,
|
requestFilter: ExecutionsQueryFilter,
|
||||||
): Promise<IExecutionsSummary[]> {
|
): Promise<IExecutionsSummary[]> {
|
||||||
let activeExecutions = [];
|
let activeExecutions = [];
|
||||||
let finishedExecutions = [];
|
|
||||||
|
|
||||||
if (!requestFilter.workflowId) {
|
if (!requestFilter.workflowId) {
|
||||||
return [];
|
return [];
|
||||||
@@ -954,10 +953,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
|
|||||||
workflowId: requestFilter.workflowId,
|
workflowId: requestFilter.workflowId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
finishedExecutions = await getFinishedExecutions(
|
const finishedExecutions = await getExecutions(rootStore.getRestApiContext, requestFilter);
|
||||||
rootStore.getRestApiContext,
|
|
||||||
requestFilter,
|
|
||||||
);
|
|
||||||
this.finishedExecutionsCount = finishedExecutions.count;
|
this.finishedExecutionsCount = finishedExecutions.count;
|
||||||
return [...activeExecutions, ...(finishedExecutions.results || [])];
|
return [...activeExecutions, ...(finishedExecutions.results || [])];
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1919,3 +1919,17 @@ export interface IExceutionSummaryNodeExecutionResult {
|
|||||||
description?: string;
|
description?: string;
|
||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExecutionOptions {
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExecutionFilters {
|
||||||
|
finished?: boolean;
|
||||||
|
mode?: WorkflowExecuteMode[];
|
||||||
|
retryOf?: string;
|
||||||
|
retrySuccessId?: string;
|
||||||
|
status?: ExecutionStatus[];
|
||||||
|
waitTill?: boolean;
|
||||||
|
workflowId?: number | string;
|
||||||
|
}
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -871,6 +871,9 @@ importers:
|
|||||||
axios:
|
axios:
|
||||||
specifier: ^0.21.1
|
specifier: ^0.21.1
|
||||||
version: 0.21.4(debug@4.3.2)
|
version: 0.21.4(debug@4.3.2)
|
||||||
|
canvas-confetti:
|
||||||
|
specifier: ^1.6.0
|
||||||
|
version: 1.6.0
|
||||||
codemirror-lang-html-n8n:
|
codemirror-lang-html-n8n:
|
||||||
specifier: ^1.0.0
|
specifier: ^1.0.0
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
@@ -1001,6 +1004,9 @@ importers:
|
|||||||
'@testing-library/vue':
|
'@testing-library/vue':
|
||||||
specifier: ^5.8.3
|
specifier: ^5.8.3
|
||||||
version: 5.8.3(vue-template-compiler@2.7.14)(vue@2.7.14)
|
version: 5.8.3(vue-template-compiler@2.7.14)(vue@2.7.14)
|
||||||
|
'@types/canvas-confetti':
|
||||||
|
specifier: ^1.6.0
|
||||||
|
version: 1.6.0
|
||||||
'@types/dateformat':
|
'@types/dateformat':
|
||||||
specifier: ^3.0.0
|
specifier: ^3.0.0
|
||||||
version: 3.0.1
|
version: 3.0.1
|
||||||
@@ -6017,6 +6023,10 @@ packages:
|
|||||||
'@types/connect': 3.4.35
|
'@types/connect': 3.4.35
|
||||||
'@types/node': 16.18.12
|
'@types/node': 16.18.12
|
||||||
|
|
||||||
|
/@types/canvas-confetti@1.6.0:
|
||||||
|
resolution: {integrity: sha512-Yq6rIccwcco0TLD5SMUrIM7Fk7Fe/C0jmNRxJJCLtAF6gebDkPuUjK5EHedxecm69Pi/aA+It39Ux4OHmFhjRw==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@types/caseless@0.12.2:
|
/@types/caseless@0.12.2:
|
||||||
resolution: {integrity: sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==}
|
resolution: {integrity: sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -8897,6 +8907,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
|
resolution: {integrity: sha512-8sdQIdMztYmzfTMO6KfLny878Ln9c2M0fc7EH60IjlP4Dc4PiCy7K2Vl3ITmWgOyPgVQKa5x+UP/KqFsxj4mBg==}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/canvas-confetti@1.6.0:
|
||||||
|
resolution: {integrity: sha512-ej+w/m8Jzpv9Z7W7uJZer14Ke8P2ogsjg4ZMGIuq4iqUOqY2Jq8BNW42iGmNfRwREaaEfFIczLuZZiEVSYNHAA==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/capital-case@1.0.4:
|
/capital-case@1.0.4:
|
||||||
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
resolution: {integrity: sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user