Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Add user activation survey #5677

Merged
merged 40 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
fa78c09
:zap: Add user activation survey
RicardoE105 Mar 13, 2023
7319cdd
Fix typo
RicardoE105 Mar 14, 2023
64a740b
Avoid showing the modal when there is a modal view
RicardoE105 Mar 14, 2023
97c598d
Allow to redirect to specific execution
RicardoE105 Mar 14, 2023
7f4d635
Improve structure
RicardoE105 Mar 14, 2023
1d85f84
Handle errors when sharing feedback
RicardoE105 Mar 14, 2023
7ad346b
update withFeatureFlag function
RicardoE105 Mar 14, 2023
29d780d
Fix linting issue
RicardoE105 Mar 15, 2023
b18d292
Set user activation flag on workflowExecutionCompleted event
RicardoE105 Mar 15, 2023
27f9adc
Revert update user settings functionality
RicardoE105 Mar 15, 2023
481f268
Remove unnecessary changes
RicardoE105 Mar 15, 2023
01e6ee8
fix linting issue
RicardoE105 Mar 15, 2023
cb98176
account for new functionality in tests
RicardoE105 Mar 15, 2023
5e53399
Small improvements
RicardoE105 Mar 15, 2023
b9a99fe
keep once instace of the model open between tabs
RicardoE105 Mar 15, 2023
a9ffb51
Add sorting to GET /executions
RicardoE105 Mar 15, 2023
683dc55
type parameters for GET /executions
RicardoE105 Mar 15, 2023
1557f54
Add constant for local store key
RicardoE105 Mar 15, 2023
6683168
Add execution mode filtering
RicardoE105 Mar 15, 2023
f0d47c3
fix linting issue
RicardoE105 Mar 15, 2023
8100372
Sync master
RicardoE105 Mar 16, 2023
c72d62e
Do not override settings when setting isOnboarded true
RicardoE105 Mar 16, 2023
99dc442
Add update user settings endpoint
RicardoE105 Mar 16, 2023
3a437f9
improvements
RicardoE105 Mar 16, 2023
868677e
revert changes to /GET executions
RicardoE105 Mar 16, 2023
da66488
Fix typo
RicardoE105 Mar 20, 2023
17fca8b
Merge branch 'master' into ado-415-p0-ask-activated-users-for-their-b…
RicardoE105 Mar 20, 2023
f776c51
Merge branch 'master' into ado-415-p0-ask-activated-users-for-their-b…
RicardoE105 Mar 29, 2023
3beede4
Add userActivated flag to user store
RicardoE105 Mar 30, 2023
c4451cb
Add E2E test
RicardoE105 Mar 30, 2023
82ff90a
Fix linting issue
RicardoE105 Apr 4, 2023
6d2b034
Sync master
RicardoE105 Apr 4, 2023
15b4717
Update pnpm-lock
RicardoE105 Apr 4, 2023
6c8b34b
Revert unnecessary change
RicardoE105 Apr 4, 2023
0a69463
Sync master
RicardoE105 Apr 7, 2023
45842dd
Centralize user's settings update
RicardoE105 Apr 7, 2023
728e931
Remove unused ref in userActivationSurvey modal
RicardoE105 Apr 7, 2023
1819ff8
Use aliased imports
RicardoE105 Apr 7, 2023
deb62fd
Use createEventBus function in component
RicardoE105 Apr 7, 2023
12244a5
Fix tests
RicardoE105 Apr 10, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions cypress/e2e/22-user-activation-modal.cy.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 90 additions & 0 deletions cypress/fixtures/Settings_user_activation_modal_enabled.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
2 changes: 2 additions & 0 deletions cypress/pages/modals/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from './credentials-modal';
export * from './message-box';
export * from './workflow-sharing-modal';
export * from './user-activation-survey-modal';

9 changes: 9 additions & 0 deletions cypress/pages/modals/user-activation-survey-modal.ts
Original file line number Diff line number Diff line change
@@ -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'),
};
}
5 changes: 5 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,7 @@ export interface IN8nUISettings {
debug: boolean;
};
personalizationSurveyEnabled: boolean;
userActivationSurveyEnabled: boolean;
defaultLocale: string;
userManagement: IUserManagementSettings;
sso: {
Expand Down Expand Up @@ -547,6 +548,9 @@ export interface IPersonalizationSurveyAnswers {

export interface IUserSettings {
isOnboarded?: boolean;
showUserActivationSurvey?: boolean;
firstSuccessfulWorkflowId?: string;
userActivated?: boolean;
}

export interface IUserManagementSettings {
Expand Down Expand Up @@ -855,6 +859,7 @@ export interface PublicUser {
globalRole?: Role;
signInType: AuthProviderType;
disabled: boolean;
settings?: IUserSettings | null;
inviteAcceptUrl?: string;
}

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,8 @@ class Server extends AbstractServer {
},
personalizationSurveyEnabled:
config.getEnv('personalization.enabled') && config.getEnv('diagnostics.enabled'),
userActivationSurveyEnabled:
config.getEnv('userActivationSurvey.enabled') && config.getEnv('diagnostics.enabled'),
defaultLocale: config.getEnv('defaultLocale'),
userManagement: {
enabled: isUserManagementEnabled(),
Expand Down Expand Up @@ -364,7 +366,6 @@ class Server extends AbstractServer {
if (config.get('nodes.packagesMissing').length > 0) {
this.frontendSettings.missingPackages = true;
}

return this.frontendSettings;
}

Expand Down
1 change: 0 additions & 1 deletion packages/cli/src/UserManagement/UserManagementHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,6 @@ export async function withFeatureFlags(

const fetchPromise = new Promise<CurrentUser>(async (resolve) => {
user.featureFlags = await postHog.getFeatureFlags(user);

resolve(user);
});

Expand Down
3 changes: 2 additions & 1 deletion packages/cli/src/WorkflowHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { whereClause } from '@/UserManagement/UserManagementHelper';
import omit from 'lodash.omit';
import { PermissionChecker } from './UserManagement/PermissionChecker';
import { isWorkflowIdValid } from './utils';
import { UserService } from './user/user.service';

const ERROR_TRIGGER_TYPE = config.getEnv('nodes.errorTriggerType');

Expand Down Expand Up @@ -429,7 +430,7 @@ export async function isBelowOnboardingThreshold(user: User): Promise<boolean> {

// user is above threshold --> set flag in settings
if (!belowThreshold) {
void Db.collections.User.update(user.id, { settings: { isOnboarded: true } });
void UserService.updateUserSettings(user.id, { isOnboarded: true });
}

return belowThreshold;
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
enabled: {
doc: 'Whether diagnostic mode is enabled.',
Expand Down
28 changes: 26 additions & 2 deletions packages/cli/src/controllers/me.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import { issueCookie } from '@/auth/jwt';
import { Response } from 'express';
import type { Repository } from 'typeorm';
import type { ILogger } from 'n8n-workflow';
import { AuthenticatedRequest, MeRequest, UserUpdatePayload } from '@/requests';
import {
AuthenticatedRequest,
MeRequest,
UserSettingsUpdatePayload,
UserUpdatePayload,
} from '@/requests';
import type {
PublicUser,
IDatabaseCollections,
Expand All @@ -23,6 +28,7 @@ import type {
} from '@/Interfaces';
import { randomBytes } from 'crypto';
import { isSamlLicensedAndEnabled } from '../sso/saml/samlHelpers';
import { UserService } from '@/user/user.service';

@RestController('/me')
export class MeController {
Expand Down Expand Up @@ -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('/')
async updateCurrentUser(req: MeRequest.UserUpdate, res: Response): Promise<PublicUser> {
Expand Down Expand Up @@ -234,4 +240,22 @@ export class MeController {

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;
}
}
10 changes: 10 additions & 0 deletions packages/cli/src/events/WorkflowStatistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { QueryFailedError } from 'typeorm';
import { Container } from 'typedi';
import { InternalHooks } from '@/InternalHooks';
import config from '@/config';
import { UserService } from '@/user/user.service';

enum StatisticsUpsertResult {
insert = 'insert',
Expand Down Expand Up @@ -112,6 +113,15 @@ export async function workflowExecutionCompleted(
user_id: owner.id,
workflow_id: workflowId,
};

if (!owner.settings?.firstSuccessfulWorkflowId) {
await UserService.updateUserSettings(owner.id, {
firstSuccessfulWorkflowId: workflowId,
userActivated: true,
showUserActivationSurvey: true,
});
}

// Send the metrics
await Container.get(InternalHooks).onFirstProductionWorkflowSuccess(metrics);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/executions/executions.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
.select([
'execution.id',
Expand Down
12 changes: 11 additions & 1 deletion packages/cli/src/requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import type {
IWorkflowSettings,
} 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 type { PublicUser, IExecutionDeleteFilter, IWorkflowDb } from '@/Interfaces';
import type { Role } from '@db/entities/Role';
Expand All @@ -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.' })
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<
RouteParams = {},
Expand Down Expand Up @@ -161,6 +170,7 @@ export declare namespace ExecutionRequest {
// ----------------------------------

export declare namespace MeRequest {
export type UserSettingsUpdate = AuthenticatedRequest<{}, {}, UserSettingsUpdatePayload>;
export type UserUpdate = AuthenticatedRequest<{}, {}, UserUpdatePayload>;
export type Password = AuthenticatedRequest<
{},
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/user/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { EntityManager, FindOptionsWhere } from 'typeorm';
import { In } from 'typeorm';
import * as Db from '@/Db';
import { User } from '@db/entities/User';
import type { IUserSettings } from '@/Interfaces';

export class UserService {
static async get(where: FindOptionsWhere<User>): Promise<User | null> {
Expand All @@ -14,4 +15,11 @@ export class UserService {
static async getByIds(transaction: EntityManager, ids: string[]) {
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 } });
}
}
Loading