mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
chore: Clean up welcome sticky feature (no-changelog) (#11987)
This commit is contained in:
@@ -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' =
|
||||||
|
|||||||
@@ -150,7 +150,6 @@ describe('GlobalConfig', () => {
|
|||||||
},
|
},
|
||||||
workflows: {
|
workflows: {
|
||||||
defaultName: 'My workflow',
|
defaultName: 'My workflow',
|
||||||
onboardingFlowDisabled: false,
|
|
||||||
callerPolicyDefaultOption: 'workflowsFromSameOwner',
|
callerPolicyDefaultOption: 'workflowsFromSameOwner',
|
||||||
},
|
},
|
||||||
endpoints: {
|
endpoints: {
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 |
@@ -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 {
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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[](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}",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user