chore: Clean up welcome sticky feature (no-changelog) (#11987)

This commit is contained in:
Mutasem Aldmour
2024-12-02 19:35:54 +01:00
committed by GitHub
parent fb5cf4beea
commit 9f78b16efc
14 changed files with 2 additions and 108 deletions

View File

@@ -6,10 +6,6 @@ export class WorkflowsConfig {
@Env('WORKFLOWS_DEFAULT_NAME') @Env('WORKFLOWS_DEFAULT_NAME')
defaultName: string = 'My workflow'; defaultName: string = 'My workflow';
/** Show onboarding flow in new workflow */
@Env('N8N_ONBOARDING_FLOW_DISABLED')
onboardingFlowDisabled: boolean = false;
/** Default option for which workflows may call the current workflow */ /** Default option for which workflows may call the current workflow */
@Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION') @Env('N8N_WORKFLOW_CALLER_POLICY_DEFAULT_OPTION')
callerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner' = callerPolicyDefaultOption: 'any' | 'none' | 'workflowsFromAList' | 'workflowsFromSameOwner' =

View File

@@ -150,7 +150,6 @@ describe('GlobalConfig', () => {
}, },
workflows: { workflows: {
defaultName: 'My workflow', defaultName: 'My workflow',
onboardingFlowDisabled: false,
callerPolicyDefaultOption: 'workflowsFromSameOwner', callerPolicyDefaultOption: 'workflowsFromSameOwner',
}, },
endpoints: { endpoints: {

View File

@@ -1,72 +0,0 @@
// eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import
import { In } from '@n8n/typeorm';
import { Service } from 'typedi';
import type { User } from '@/databases/entities/user';
import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository';
import { WorkflowRepository } from '@/databases/repositories/workflow.repository';
import { UserService } from '@/services/user.service';
@Service()
export class UserOnboardingService {
constructor(
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
private readonly workflowRepository: WorkflowRepository,
private readonly userService: UserService,
) {}
/**
* Check if user owns more than 15 workflows or more than 2 workflows with at least 2 nodes.
* If user does, set flag in its settings.
*/
async isBelowThreshold(user: User): Promise<boolean> {
let belowThreshold = true;
const skippedTypes = ['n8n-nodes-base.start', 'n8n-nodes-base.stickyNote'];
const ownedWorkflowsIds = await this.sharedWorkflowRepository
.find({
where: {
project: {
projectRelations: {
role: 'project:personalOwner',
userId: user.id,
},
},
role: 'workflow:owner',
},
select: ['workflowId'],
})
.then((ownedWorkflows) => ownedWorkflows.map(({ workflowId }) => workflowId));
if (ownedWorkflowsIds.length > 15) {
belowThreshold = false;
} else {
// just fetch workflows' nodes to keep memory footprint low
const workflows = await this.workflowRepository.find({
where: { id: In(ownedWorkflowsIds) },
select: ['nodes'],
});
// valid workflow: 2+ nodes without start node
const validWorkflowCount = workflows.reduce((counter, workflow) => {
if (counter <= 2 && workflow.nodes.length > 2) {
const nodes = workflow.nodes.filter((node) => !skippedTypes.includes(node.type));
if (nodes.length >= 2) {
return counter + 1;
}
}
return counter;
}, 0);
// more than 2 valid workflows required
belowThreshold = validWorkflowCount <= 2;
}
// user is above threshold --> set flag in settings
if (!belowThreshold) {
void this.userService.updateSettings(user.id, { isOnboarded: true });
}
return belowThreshold;
}
}

View File

@@ -33,7 +33,6 @@ import * as ResponseHelper from '@/response-helper';
import { NamingService } from '@/services/naming.service'; import { NamingService } from '@/services/naming.service';
import { ProjectService } from '@/services/project.service'; import { ProjectService } from '@/services/project.service';
import { TagService } from '@/services/tag.service'; import { TagService } from '@/services/tag.service';
import { UserOnboardingService } from '@/services/user-onboarding.service';
import { UserManagementMailer } from '@/user-management/email'; import { UserManagementMailer } from '@/user-management/email';
import * as utils from '@/utils'; import * as utils from '@/utils';
import * as WorkflowHelpers from '@/workflow-helpers'; import * as WorkflowHelpers from '@/workflow-helpers';
@@ -55,7 +54,6 @@ export class WorkflowsController {
private readonly workflowHistoryService: WorkflowHistoryService, private readonly workflowHistoryService: WorkflowHistoryService,
private readonly tagService: TagService, private readonly tagService: TagService,
private readonly namingService: NamingService, private readonly namingService: NamingService,
private readonly userOnboardingService: UserOnboardingService,
private readonly workflowRepository: WorkflowRepository, private readonly workflowRepository: WorkflowRepository,
private readonly workflowService: WorkflowService, private readonly workflowService: WorkflowService,
private readonly workflowExecutionService: WorkflowExecutionService, private readonly workflowExecutionService: WorkflowExecutionService,
@@ -213,13 +211,7 @@ export class WorkflowsController {
const requestedName = req.query.name ?? this.globalConfig.workflows.defaultName; const requestedName = req.query.name ?? this.globalConfig.workflows.defaultName;
const name = await this.namingService.getUniqueWorkflowName(requestedName); const name = await this.namingService.getUniqueWorkflowName(requestedName);
return { name };
const onboardingFlowEnabled =
!this.globalConfig.workflows.onboardingFlowDisabled &&
!req.user.settings?.isOnboarded &&
(await this.userOnboardingService.isBelowThreshold(req.user));
return { name, onboardingFlowEnabled };
} }
@Get('/from-url') @Get('/from-url')

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

View File

@@ -250,7 +250,6 @@ export interface IWorkflowToShare extends IWorkflowDataUpdate {
export interface NewWorkflowResponse { export interface NewWorkflowResponse {
name: string; name: string;
onboardingFlowEnabled?: boolean;
defaultSettings: IWorkflowSettings; defaultSettings: IWorkflowSettings;
} }
@@ -277,7 +276,6 @@ export interface IWorkflowTemplate {
export interface INewWorkflowData { export interface INewWorkflowData {
name: string; name: string;
onboardingFlowEnabled: boolean;
} }
export interface WorkflowMetadata { export interface WorkflowMetadata {

View File

@@ -22,7 +22,6 @@ export async function getNewWorkflow(context: IRestApiContext, data?: IDataObjec
); );
return { return {
name: response.name, name: response.name,
onboardingFlowEnabled: response.onboardingFlowEnabled === true,
settings: response.defaultSettings, settings: response.defaultSettings,
}; };
} }

View File

@@ -7,7 +7,6 @@ import type { Workflow } from 'n8n-workflow';
import { isNumber, isString } from '@/utils/typeGuards'; import { isNumber, isString } from '@/utils/typeGuards';
import type { INodeUi, XYPosition } from '@/Interface'; import type { INodeUi, XYPosition } from '@/Interface';
import { QUICKSTART_NOTE_NAME } from '@/constants';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -205,16 +204,7 @@ const onEdit = (edit: boolean) => {
const onMarkdownClick = (link: HTMLAnchorElement) => { const onMarkdownClick = (link: HTMLAnchorElement) => {
if (link) { if (link) {
const isOnboardingNote = props.name === QUICKSTART_NOTE_NAME; telemetry.track('User clicked note link', { type: 'other' });
const isWelcomeVideo = link.querySelector('img[alt="n8n quickstart video"]');
const type =
isOnboardingNote && isWelcomeVideo
? 'welcome_video'
: isOnboardingNote && link.getAttribute('href') === '/templates'
? 'templates'
: 'other';
telemetry.track('User clicked note link', { type });
} }
}; };

View File

@@ -25,7 +25,6 @@ import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { import {
EnterpriseEditionFeature, EnterpriseEditionFeature,
FORM_TRIGGER_NODE_TYPE, FORM_TRIGGER_NODE_TYPE,
QUICKSTART_NOTE_NAME,
STICKY_NODE_TYPE, STICKY_NODE_TYPE,
UPDATE_WEBHOOK_ID_NODE_TYPES, UPDATE_WEBHOOK_ID_NODE_TYPES,
WEBHOOK_NODE_TYPE, WEBHOOK_NODE_TYPE,
@@ -365,7 +364,6 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
if (node.type === STICKY_NODE_TYPE) { if (node.type === STICKY_NODE_TYPE) {
telemetry.track('User deleted workflow note', { telemetry.track('User deleted workflow note', {
workflow_id: workflowsStore.workflowId, workflow_id: workflowsStore.workflowId,
is_welcome_note: node.name === QUICKSTART_NOTE_NAME,
}); });
} else { } else {
void externalHooks.run('node.deleteNode', { node }); void externalHooks.run('node.deleteNode', { node });

View File

@@ -35,7 +35,6 @@ export const MIN_WORKFLOW_NAME_LENGTH = 1;
export const MAX_WORKFLOW_NAME_LENGTH = 128; export const MAX_WORKFLOW_NAME_LENGTH = 128;
export const DUPLICATE_POSTFFIX = ' copy'; export const DUPLICATE_POSTFFIX = ' copy';
export const NODE_OUTPUT_DEFAULT_KEY = '_NODE_OUTPUT_DEFAULT_KEY_'; export const NODE_OUTPUT_DEFAULT_KEY = '_NODE_OUTPUT_DEFAULT_KEY_';
export const QUICKSTART_NOTE_NAME = '_QUICKSTART_NOTE_';
// tags // tags
export const MAX_TAG_NAME_LENGTH = 24; export const MAX_TAG_NAME_LENGTH = 24;

View File

@@ -1381,7 +1381,6 @@
"nodeWebhooks.webhookUrls": "Webhook URLs", "nodeWebhooks.webhookUrls": "Webhook URLs",
"nodeWebhooks.webhookUrls.formTrigger": "Form URLs", "nodeWebhooks.webhookUrls.formTrigger": "Form URLs",
"nodeWebhooks.webhookUrls.chatTrigger": "Chat URL", "nodeWebhooks.webhookUrls.chatTrigger": "Chat URL",
"onboardingWorkflow.stickyContent": "## 👇 Get started faster \nLightning tour of the key concepts [4 min] \n\n[![n8n quickstart video](/static/quickstart_thumbnail.png#full-width)](https://www.youtube.com/watch?v=1MwSoB0gnM4)",
"openWorkflow.workflowImportError": "Could not import workflow", "openWorkflow.workflowImportError": "Could not import workflow",
"openWorkflow.workflowNotFoundError": "Could not find workflow", "openWorkflow.workflowNotFoundError": "Could not find workflow",
"parameterInput.expressionResult": "e.g. {result}", "parameterInput.expressionResult": "e.g. {result}",

View File

@@ -468,7 +468,6 @@ describe('useWorkflowsStore', () => {
const expectedName = `${name}${DUPLICATE_POSTFFIX}`; const expectedName = `${name}${DUPLICATE_POSTFFIX}`;
vi.mocked(workflowsApi).getNewWorkflow.mockResolvedValue({ vi.mocked(workflowsApi).getNewWorkflow.mockResolvedValue({
name: expectedName, name: expectedName,
onboardingFlowEnabled: false,
settings: {} as IWorkflowSettings, settings: {} as IWorkflowSettings,
}); });
const newName = await workflowsStore.getDuplicateCurrentWorkflowName(name); const newName = await workflowsStore.getDuplicateCurrentWorkflowName(name);

View File

@@ -494,7 +494,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
async function getNewWorkflowData(name?: string, projectId?: string): Promise<INewWorkflowData> { async function getNewWorkflowData(name?: string, projectId?: string): Promise<INewWorkflowData> {
let workflowData = { let workflowData = {
name: '', name: '',
onboardingFlowEnabled: false,
settings: { ...defaults.settings }, settings: { ...defaults.settings },
}; };
try { try {

View File

@@ -24,7 +24,6 @@ import {
MODAL_CANCEL, MODAL_CANCEL,
MODAL_CONFIRM, MODAL_CONFIRM,
PLACEHOLDER_EMPTY_WORKFLOW_ID, PLACEHOLDER_EMPTY_WORKFLOW_ID,
QUICKSTART_NOTE_NAME,
START_NODE_TYPE, START_NODE_TYPE,
STICKY_NODE_TYPE, STICKY_NODE_TYPE,
VIEWS, VIEWS,
@@ -3581,7 +3580,6 @@ export default defineComponent({
if (node.type === STICKY_NODE_TYPE) { if (node.type === STICKY_NODE_TYPE) {
this.$telemetry.track('User deleted workflow note', { this.$telemetry.track('User deleted workflow note', {
workflow_id: this.workflowsStore.workflowId, workflow_id: this.workflowsStore.workflowId,
is_welcome_note: node.name === QUICKSTART_NOTE_NAME,
}); });
} else { } else {
void this.externalHooks.run('node.deleteNode', { node }); void this.externalHooks.run('node.deleteNode', { node });