mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(core): Add optional context parameter to track creation source for workflows, credentials, and projects (#18736)
Co-authored-by: r00gm <raul00gm@gmail.com>
This commit is contained in:
@@ -6,4 +6,5 @@ export class CreateCredentialDto extends Z.class({
|
|||||||
type: z.string().min(1).max(128),
|
type: z.string().min(1).max(128),
|
||||||
data: z.record(z.string(), z.unknown()),
|
data: z.record(z.string(), z.unknown()),
|
||||||
projectId: z.string().optional(),
|
projectId: z.string().optional(),
|
||||||
|
uiContext: z.string().optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
import { Z } from 'zod-class';
|
import { Z } from 'zod-class';
|
||||||
|
|
||||||
import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema';
|
import { projectIconSchema, projectNameSchema } from '../../schemas/project.schema';
|
||||||
@@ -5,4 +6,5 @@ import { projectIconSchema, projectNameSchema } from '../../schemas/project.sche
|
|||||||
export class CreateProjectDto extends Z.class({
|
export class CreateProjectDto extends Z.class({
|
||||||
name: projectNameSchema,
|
name: projectNameSchema,
|
||||||
icon: projectIconSchema.optional(),
|
icon: projectIconSchema.optional(),
|
||||||
|
uiContext: z.string().optional(),
|
||||||
}) {}
|
}) {}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export class ProjectController {
|
|||||||
this.eventService.emit('team-project-created', {
|
this.eventService.emit('team-project-created', {
|
||||||
userId: req.user.id,
|
userId: req.user.id,
|
||||||
role: req.user.role.slug,
|
role: req.user.role.slug,
|
||||||
|
uiContext: payload.uiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ describe('CredentialsController', () => {
|
|||||||
projectId: projectOwningCredentialData.id,
|
projectId: projectOwningCredentialData.id,
|
||||||
projectType: projectOwningCredentialData.type,
|
projectType: projectOwningCredentialData.type,
|
||||||
publicApi: false,
|
publicApi: false,
|
||||||
|
uiContext: newCredentialsPayload.uiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(newApiKey).toEqual(createdCredentials);
|
expect(newApiKey).toEqual(createdCredentials);
|
||||||
|
|||||||
@@ -192,6 +192,7 @@ export class CredentialsController {
|
|||||||
publicApi: false,
|
publicApi: false,
|
||||||
projectId: project?.id,
|
projectId: project?.id,
|
||||||
projectType: project?.type,
|
projectType: project?.type,
|
||||||
|
uiContext: payload.uiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
return newCredential;
|
return newCredential;
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ export type RelayEventMap = {
|
|||||||
publicApi: boolean;
|
publicApi: boolean;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
projectType: string;
|
projectType: string;
|
||||||
|
uiContext?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
'workflow-deleted': {
|
'workflow-deleted': {
|
||||||
@@ -292,6 +293,7 @@ export type RelayEventMap = {
|
|||||||
publicApi: boolean;
|
publicApi: boolean;
|
||||||
projectId?: string;
|
projectId?: string;
|
||||||
projectType?: string;
|
projectType?: string;
|
||||||
|
uiContext?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
'credentials-shared': {
|
'credentials-shared': {
|
||||||
@@ -385,6 +387,7 @@ export type RelayEventMap = {
|
|||||||
'team-project-created': {
|
'team-project-created': {
|
||||||
userId: string;
|
userId: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
uiContext?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
// #endregion
|
// #endregion
|
||||||
|
|||||||
@@ -149,10 +149,11 @@ export class TelemetryEventRelay extends EventRelay {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private teamProjectCreated({ userId, role }: RelayEventMap['team-project-created']) {
|
private teamProjectCreated({ userId, role, uiContext }: RelayEventMap['team-project-created']) {
|
||||||
this.telemetry.track('User created project', {
|
this.telemetry.track('User created project', {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
role,
|
role,
|
||||||
|
uiContext,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +405,7 @@ export class TelemetryEventRelay extends EventRelay {
|
|||||||
credentialId,
|
credentialId,
|
||||||
projectId,
|
projectId,
|
||||||
projectType,
|
projectType,
|
||||||
|
uiContext,
|
||||||
}: RelayEventMap['credentials-created']) {
|
}: RelayEventMap['credentials-created']) {
|
||||||
this.telemetry.track('User created credentials', {
|
this.telemetry.track('User created credentials', {
|
||||||
user_id: user.id,
|
user_id: user.id,
|
||||||
@@ -411,6 +413,7 @@ export class TelemetryEventRelay extends EventRelay {
|
|||||||
credential_id: credentialId,
|
credential_id: credentialId,
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
project_type: projectType,
|
project_type: projectType,
|
||||||
|
uiContext,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +527,7 @@ export class TelemetryEventRelay extends EventRelay {
|
|||||||
publicApi,
|
publicApi,
|
||||||
projectId,
|
projectId,
|
||||||
projectType,
|
projectType,
|
||||||
|
uiContext,
|
||||||
}: RelayEventMap['workflow-created']) {
|
}: RelayEventMap['workflow-created']) {
|
||||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
|
|
||||||
@@ -534,6 +538,7 @@ export class TelemetryEventRelay extends EventRelay {
|
|||||||
public_api: publicApi,
|
public_api: publicApi,
|
||||||
project_id: projectId,
|
project_id: projectId,
|
||||||
project_type: projectType,
|
project_type: projectType,
|
||||||
|
uiContext,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export declare namespace WorkflowRequest {
|
|||||||
meta: Record<string, unknown>;
|
meta: Record<string, unknown>;
|
||||||
projectId: string;
|
projectId: string;
|
||||||
parentFolderId?: string;
|
parentFolderId?: string;
|
||||||
|
uiContext?: string;
|
||||||
}>;
|
}>;
|
||||||
|
|
||||||
type ManualRunPayload = {
|
type ManualRunPayload = {
|
||||||
|
|||||||
@@ -218,6 +218,7 @@ export class WorkflowsController {
|
|||||||
publicApi: false,
|
publicApi: false,
|
||||||
projectId: project!.id,
|
projectId: project!.id,
|
||||||
projectType: project!.type,
|
projectType: project!.type,
|
||||||
|
uiContext: req.body.uiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
|
const scopes = await this.workflowService.getWorkflowScopes(req.user, savedWorkflow.id);
|
||||||
|
|||||||
@@ -822,6 +822,25 @@ describe('POST /credentials', () => {
|
|||||||
expect(sharedCredential.credentials.name).toBe(payload.name);
|
expect(sharedCredential.credentials.name).toBe(payload.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create cred with uiContext parameter', async () => {
|
||||||
|
const payload = { ...randomCredentialPayload(), uiContext: 'credentials_list' };
|
||||||
|
|
||||||
|
const response = await authMemberAgent.post('/credentials').send(payload);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const { id, name, type } = response.body.data;
|
||||||
|
|
||||||
|
expect(name).toBe(payload.name);
|
||||||
|
expect(type).toBe(payload.type);
|
||||||
|
|
||||||
|
const credential = await getCredentialById(id);
|
||||||
|
a.ok(credential);
|
||||||
|
|
||||||
|
expect(credential.name).toBe(payload.name);
|
||||||
|
expect(credential.type).toBe(payload.type);
|
||||||
|
});
|
||||||
|
|
||||||
test('should fail with invalid inputs', async () => {
|
test('should fail with invalid inputs', async () => {
|
||||||
for (const invalidPayload of INVALID_PAYLOADS) {
|
for (const invalidPayload of INVALID_PAYLOADS) {
|
||||||
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
|
const response = await authOwnerAgent.post('/credentials').send(invalidPayload);
|
||||||
|
|||||||
@@ -398,6 +398,23 @@ describe('POST /projects/', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create a team project with context parameter', async () => {
|
||||||
|
const ownerUser = await createOwner();
|
||||||
|
const ownerAgent = testServer.authAgentFor(ownerUser);
|
||||||
|
|
||||||
|
const resp = await ownerAgent.post('/projects/').send({
|
||||||
|
name: 'Test Team Project with Context',
|
||||||
|
uiContext: 'universal_button',
|
||||||
|
});
|
||||||
|
expect(resp.status).toBe(200);
|
||||||
|
const respProject = resp.body.data as Project;
|
||||||
|
expect(respProject.name).toEqual('Test Team Project with Context');
|
||||||
|
expect(async () => {
|
||||||
|
await findProject(respProject.id);
|
||||||
|
}).not.toThrow();
|
||||||
|
expect(resp.body.data.role).toBe('project:admin');
|
||||||
|
});
|
||||||
|
|
||||||
test('should allow to create a team projects if below the quota', async () => {
|
test('should allow to create a team projects if below the quota', async () => {
|
||||||
testServer.license.setQuota('quota:maxTeamProjects', 1);
|
testServer.license.setQuota('quota:maxTeamProjects', 1);
|
||||||
const ownerUser = await createOwner();
|
const ownerUser = await createOwner();
|
||||||
|
|||||||
@@ -145,6 +145,38 @@ describe('POST /workflows', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should create workflow with uiContext parameter', async () => {
|
||||||
|
const payload = {
|
||||||
|
name: 'testing with context',
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
id: 'uuid-1234',
|
||||||
|
parameters: {},
|
||||||
|
name: 'Start',
|
||||||
|
type: 'n8n-nodes-base.start',
|
||||||
|
typeVersion: 1,
|
||||||
|
position: [240, 300],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
connections: {},
|
||||||
|
staticData: null,
|
||||||
|
settings: {},
|
||||||
|
active: false,
|
||||||
|
uiContext: 'workflow_list',
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await authMemberAgent.post('/workflows').send(payload);
|
||||||
|
|
||||||
|
expect(response.statusCode).toBe(200);
|
||||||
|
|
||||||
|
const {
|
||||||
|
data: { id, name },
|
||||||
|
} = response.body;
|
||||||
|
|
||||||
|
expect(id).toBeDefined();
|
||||||
|
expect(name).toBe('testing with context');
|
||||||
|
});
|
||||||
|
|
||||||
test('should create workflow history version when licensed', async () => {
|
test('should create workflow history version when licensed', async () => {
|
||||||
license.enable('feat:workflowHistory');
|
license.enable('feat:workflowHistory');
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface WorkflowDataUpdate {
|
|||||||
versionId?: string;
|
versionId?: string;
|
||||||
meta?: WorkflowMetadata;
|
meta?: WorkflowMetadata;
|
||||||
parentFolderId?: string;
|
parentFolderId?: string;
|
||||||
|
uiContext?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WorkflowDataCreate extends WorkflowDataUpdate {
|
export interface WorkflowDataCreate extends WorkflowDataUpdate {
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ import {
|
|||||||
import { isCredentialModalState, isValidCredentialResponse } from '@/utils/typeGuards';
|
import { isCredentialModalState, isValidCredentialResponse } from '@/utils/typeGuards';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { useElementSize } from '@vueuse/core';
|
import { useElementSize } from '@vueuse/core';
|
||||||
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modalName: string;
|
modalName: string;
|
||||||
@@ -75,6 +76,7 @@ const toast = useToast();
|
|||||||
const message = useMessage();
|
const message = useMessage();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
const activeTab = ref('connection');
|
const activeTab = ref('connection');
|
||||||
const authError = ref('');
|
const authError = ref('');
|
||||||
@@ -801,7 +803,16 @@ async function createCredential(
|
|||||||
let credential;
|
let credential;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
credential = await credentialsStore.createNewCredential(credentialDetails, project?.id);
|
credential = await credentialsStore.createNewCredential(
|
||||||
|
credentialDetails,
|
||||||
|
project?.id,
|
||||||
|
router.currentRoute.value.query.uiContext?.toString(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { uiContext, ...rest } = router.currentRoute.value.query;
|
||||||
|
void router.replace({ query: rest });
|
||||||
|
|
||||||
hasUnsavedChanges.value = false;
|
hasUnsavedChanges.value = false;
|
||||||
|
|
||||||
const { title, message } = createToastMessagingForNewCredentials(project);
|
const { title, message } = createToastMessagingForNewCredentials(project);
|
||||||
|
|||||||
@@ -266,13 +266,15 @@ describe('ProjectHeader', () => {
|
|||||||
|
|
||||||
await userEvent.click(getByTestId('action-credential'));
|
await userEvent.click(getByTestId('action-credential'));
|
||||||
|
|
||||||
expect(mockPush).toHaveBeenCalledWith({
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
name: VIEWS.PROJECTS_CREDENTIALS,
|
expect.objectContaining({
|
||||||
params: {
|
name: VIEWS.PROJECTS_CREDENTIALS,
|
||||||
projectId: project.id,
|
params: {
|
||||||
credentialId: 'create',
|
projectId: project.id,
|
||||||
},
|
credentialId: 'create',
|
||||||
});
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -162,6 +162,37 @@ const showProjectIcon = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isCredentialsListView(routeName: string) {
|
||||||
|
const CREDENTIAL_VIEWS: string[] = [
|
||||||
|
VIEWS.PROJECTS_CREDENTIALS,
|
||||||
|
VIEWS.CREDENTIALS,
|
||||||
|
VIEWS.SHARED_CREDENTIALS,
|
||||||
|
];
|
||||||
|
|
||||||
|
return CREDENTIAL_VIEWS.includes(routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWorkflowListView(routeName: string) {
|
||||||
|
const WORKFLOWS_VIEWS: string[] = [
|
||||||
|
VIEWS.PROJECTS_WORKFLOWS,
|
||||||
|
VIEWS.WORKFLOWS,
|
||||||
|
VIEWS.SHARED_WORKFLOWS,
|
||||||
|
VIEWS.PROJECTS_FOLDERS,
|
||||||
|
];
|
||||||
|
|
||||||
|
return WORKFLOWS_VIEWS.includes(routeName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getUIContext(routeName: string) {
|
||||||
|
if (isCredentialsListView(routeName)) {
|
||||||
|
return 'credentials_list';
|
||||||
|
} else if (isWorkflowListView(routeName)) {
|
||||||
|
return 'workflow_list';
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const actions: Record<ActionTypes, (projectId: string) => void> = {
|
const actions: Record<ActionTypes, (projectId: string) => void> = {
|
||||||
[ACTION_TYPES.WORKFLOW]: (projectId: string) => {
|
[ACTION_TYPES.WORKFLOW]: (projectId: string) => {
|
||||||
void router.push({
|
void router.push({
|
||||||
@@ -169,6 +200,7 @@ const actions: Record<ActionTypes, (projectId: string) => void> = {
|
|||||||
query: {
|
query: {
|
||||||
projectId,
|
projectId,
|
||||||
parentFolderId: route.params.folderId as string,
|
parentFolderId: route.params.folderId as string,
|
||||||
|
uiContext: getUIContext(route.name?.toString() ?? ''),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -179,6 +211,9 @@ const actions: Record<ActionTypes, (projectId: string) => void> = {
|
|||||||
projectId,
|
projectId,
|
||||||
credentialId: 'create',
|
credentialId: 'create',
|
||||||
},
|
},
|
||||||
|
query: {
|
||||||
|
uiContext: getUIContext(route.name?.toString() ?? ''),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[ACTION_TYPES.FOLDER]: () => {
|
[ACTION_TYPES.FOLDER]: () => {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, onBeforeMount } from 'vue';
|
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
|
||||||
import type { IMenuItem } from '@n8n/design-system/types';
|
|
||||||
import { useI18n } from '@n8n/i18n';
|
|
||||||
import { VIEWS } from '@/constants';
|
import { VIEWS } from '@/constants';
|
||||||
import { useProjectsStore } from '@/stores/projects.store';
|
import { useProjectsStore } from '@/stores/projects.store';
|
||||||
import type { ProjectListItem } from '@/types/projects.types';
|
|
||||||
import { useGlobalEntityCreation } from '@/composables/useGlobalEntityCreation';
|
|
||||||
import { useSettingsStore } from '@/stores/settings.store';
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
|
import type { ProjectListItem } from '@/types/projects.types';
|
||||||
|
import type { IMenuItem } from '@n8n/design-system/types';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
import { ElMenu } from 'element-plus';
|
import { ElMenu } from 'element-plus';
|
||||||
|
import { computed, onBeforeMount } from 'vue';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
collapsed: boolean;
|
collapsed: boolean;
|
||||||
@@ -28,7 +28,7 @@ const isCreatingProject = computed(() => globalEntityCreation.isCreatingProject.
|
|||||||
const displayProjects = computed(() => globalEntityCreation.displayProjects.value);
|
const displayProjects = computed(() => globalEntityCreation.displayProjects.value);
|
||||||
const isFoldersFeatureEnabled = computed(() => settingsStore.isFoldersFeatureEnabled);
|
const isFoldersFeatureEnabled = computed(() => settingsStore.isFoldersFeatureEnabled);
|
||||||
const hasMultipleVerifiedUsers = computed(
|
const hasMultipleVerifiedUsers = computed(
|
||||||
() => usersStore.allUsers.filter((user) => user.isPendingUser === false).length > 1,
|
() => usersStore.allUsers.filter((user) => !user.isPendingUser).length > 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
const home = computed<IMenuItem>(() => ({
|
const home = computed<IMenuItem>(() => ({
|
||||||
@@ -140,7 +140,7 @@ onBeforeMount(async () => {
|
|||||||
data-test-id="project-plus-button"
|
data-test-id="project-plus-button"
|
||||||
:disabled="isCreatingProject || !projectsStore.hasPermissionToCreateProjects"
|
:disabled="isCreatingProject || !projectsStore.hasPermissionToCreateProjects"
|
||||||
:class="$style.plusBtn"
|
:class="$style.plusBtn"
|
||||||
@click="globalEntityCreation.createProject"
|
@click="globalEntityCreation.createProject('add_icon')"
|
||||||
/>
|
/>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
|
|||||||
@@ -184,13 +184,14 @@ export const useGlobalEntityCreation = () => {
|
|||||||
] satisfies Item[];
|
] satisfies Item[];
|
||||||
});
|
});
|
||||||
|
|
||||||
const createProject = async () => {
|
const createProject = async (uiContext?: string) => {
|
||||||
isCreatingProject.value = true;
|
isCreatingProject.value = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newProject = await projectsStore.createProject({
|
const newProject = await projectsStore.createProject({
|
||||||
name: i18n.baseText('projects.settings.newProjectName'),
|
name: i18n.baseText('projects.settings.newProjectName'),
|
||||||
icon: { type: 'icon', value: DEFAULT_ICON },
|
icon: { type: 'icon', value: DEFAULT_ICON },
|
||||||
|
uiContext,
|
||||||
});
|
});
|
||||||
await router.push({ name: VIEWS.PROJECT_SETTINGS, params: { projectId: newProject.id } });
|
await router.push({ name: VIEWS.PROJECT_SETTINGS, params: { projectId: newProject.id } });
|
||||||
toast.showMessage({
|
toast.showMessage({
|
||||||
@@ -210,7 +211,7 @@ export const useGlobalEntityCreation = () => {
|
|||||||
if (id !== CREATE_PROJECT_ID) return;
|
if (id !== CREATE_PROJECT_ID) return;
|
||||||
|
|
||||||
if (projectsStore.canCreateProjects && projectsStore.hasPermissionToCreateProjects) {
|
if (projectsStore.canCreateProjects && projectsStore.hasPermissionToCreateProjects) {
|
||||||
void createProject();
|
void createProject('universal_button');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
import { useNpsSurveyStore } from '@/stores/npsSurvey.store';
|
||||||
import { useUIStore } from '@/stores/ui.store';
|
import { useUIStore } from '@/stores/ui.store';
|
||||||
import type { NavigationGuardNext, useRouter } from 'vue-router';
|
import type { LocationQuery, NavigationGuardNext, useRouter } from 'vue-router';
|
||||||
import { useMessage } from './useMessage';
|
import { useMessage } from './useMessage';
|
||||||
import { useI18n } from '@n8n/i18n';
|
import { useI18n } from '@n8n/i18n';
|
||||||
import {
|
import {
|
||||||
@@ -156,6 +156,13 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getQueryParam(query: LocationQuery, key: string): string | undefined {
|
||||||
|
const value = query[key];
|
||||||
|
if (Array.isArray(value)) return value[0] ?? undefined;
|
||||||
|
if (value === null) return undefined;
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
async function saveCurrentWorkflow(
|
async function saveCurrentWorkflow(
|
||||||
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
|
{ id, name, tags }: { id?: string; name?: string; tags?: string[] } = {},
|
||||||
redirect = true,
|
redirect = true,
|
||||||
@@ -167,11 +174,12 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isLoading = useCanvasStore().isLoading;
|
const isLoading = useCanvasStore().isLoading;
|
||||||
const currentWorkflow = id || (router.currentRoute.value.params.name as string);
|
const currentWorkflow = id ?? getQueryParam(router.currentRoute.value.params, 'name');
|
||||||
const parentFolderId = router.currentRoute.value.query.parentFolderId as string;
|
const parentFolderId = getQueryParam(router.currentRoute.value.query, 'parentFolderId');
|
||||||
|
const uiContext = getQueryParam(router.currentRoute.value.query, 'uiContext');
|
||||||
|
|
||||||
if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) {
|
if (!currentWorkflow || ['new', PLACEHOLDER_EMPTY_WORKFLOW_ID].includes(currentWorkflow)) {
|
||||||
return !!(await saveAsNewWorkflow({ name, tags, parentFolderId }, redirect));
|
return !!(await saveAsNewWorkflow({ name, tags, parentFolderId, uiContext }, redirect));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Workflow exists already so update it
|
// Workflow exists already so update it
|
||||||
@@ -292,6 +300,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
|||||||
resetNodeIds,
|
resetNodeIds,
|
||||||
openInNewWindow,
|
openInNewWindow,
|
||||||
parentFolderId,
|
parentFolderId,
|
||||||
|
uiContext,
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -300,6 +309,7 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
|||||||
openInNewWindow?: boolean;
|
openInNewWindow?: boolean;
|
||||||
resetNodeIds?: boolean;
|
resetNodeIds?: boolean;
|
||||||
parentFolderId?: string;
|
parentFolderId?: string;
|
||||||
|
uiContext?: string;
|
||||||
data?: WorkflowDataCreate;
|
data?: WorkflowDataCreate;
|
||||||
} = {},
|
} = {},
|
||||||
redirect = true,
|
redirect = true,
|
||||||
@@ -340,6 +350,11 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
|
|||||||
if (parentFolderId) {
|
if (parentFolderId) {
|
||||||
workflowDataRequest.parentFolderId = parentFolderId;
|
workflowDataRequest.parentFolderId = parentFolderId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (uiContext) {
|
||||||
|
workflowDataRequest.uiContext = uiContext;
|
||||||
|
}
|
||||||
|
|
||||||
const workflowData = await workflowsStore.createNewWorkflow(workflowDataRequest);
|
const workflowData = await workflowsStore.createNewWorkflow(workflowDataRequest);
|
||||||
|
|
||||||
workflowsStore.addWorkflow(workflowData);
|
workflowsStore.addWorkflow(workflowData);
|
||||||
|
|||||||
@@ -317,6 +317,7 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
|||||||
const createNewCredential = async (
|
const createNewCredential = async (
|
||||||
data: ICredentialsDecrypted,
|
data: ICredentialsDecrypted,
|
||||||
projectId?: string,
|
projectId?: string,
|
||||||
|
uiContext?: string,
|
||||||
): Promise<ICredentialsResponse> => {
|
): Promise<ICredentialsResponse> => {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const credential = await credentialsApi.createNewCredential(rootStore.restApiContext, {
|
const credential = await credentialsApi.createNewCredential(rootStore.restApiContext, {
|
||||||
@@ -324,6 +325,7 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, () => {
|
|||||||
type: data.type,
|
type: data.type,
|
||||||
data: data.data ?? {},
|
data: data.data ?? {},
|
||||||
projectId,
|
projectId,
|
||||||
|
uiContext,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.homeProject && !credential.homeProject) {
|
if (data?.homeProject && !credential.homeProject) {
|
||||||
|
|||||||
Reference in New Issue
Block a user