feat(editor): Edit workflow descriptions from the MCP page (#22967)

This commit is contained in:
Milorad FIlipović
2025-12-11 09:04:16 +01:00
committed by GitHub
parent 9bffc72c8c
commit 1c2e89fa52
5 changed files with 114 additions and 2 deletions
@@ -2381,7 +2381,8 @@
"settings.mcp.tabs.workflows": "Workflows",
"settings.mcp.tabs.oauth": "Connected clients",
"settings.mcp.access.token.notice": "Make sure to copy your access token, you won't be able to see it again",
"settings.mcp.workflows.table.action.removeMCPAccess": "Remove Access",
"settings.mcp.workflows.table.action.removeMCPAccess": "Remove access",
"settings.mcp.workflows.table.action.updateDescription": "Edit description",
"settings.mcp.workflows.table.column.name": "Name",
"settings.mcp.workflows.table.column.location": "Location",
"settings.mcp.workflows.table.column.description.emptyContent": "No description",
@@ -16,6 +16,7 @@ const props = defineProps<{
data: {
workflowId: string;
workflowDescription?: string | null;
onSave?: (description: string | null) => void;
};
}>();
@@ -57,6 +58,9 @@ const saveDescription = async () => {
props.data.workflowId,
normalizedCurrentValue.value ?? null,
);
props.data.onSave?.(normalizedCurrentValue.value ?? null);
telemetry.track('User set workflow description', {
workflow_id: props.data.workflowId,
description: normalizedCurrentValue.value ?? null,
@@ -7,6 +7,7 @@ import { useI18n } from '@n8n/i18n';
import { computed, onMounted, ref } from 'vue';
import { useMCPStore } from '@/features/ai/mcpAccess/mcp.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { useUIStore } from '@/app/stores/ui.store';
import { LOADING_INDICATOR_TIMEOUT } from '@/features/ai/mcpAccess/mcp.constants';
import MCPEmptyState from '@/features/ai/mcpAccess/components/MCPEmptyState.vue';
import MCpHeaderActions from '@/features/ai/mcpAccess/components/header/MCPHeaderActions.vue';
@@ -16,6 +17,7 @@ import { N8nHeading, N8nTabs, N8nTooltip, N8nButton } from '@n8n/design-system';
import type { TabOptions } from '@n8n/design-system';
import { useMcp } from '@/features/ai/mcpAccess/composables/useMcp';
import type { OAuthClientResponseDto } from '@n8n/api-types';
import { WORKFLOW_DESCRIPTION_MODAL_KEY } from '@/app/constants';
type MCPTabs = 'workflows' | 'oauth';
@@ -27,6 +29,7 @@ const mcp = useMcp();
const workflowsStore = useWorkflowsStore();
const mcpStore = useMCPStore();
const usersStore = useUsersStore();
const uiStore = useUIStore();
const mcpStatusLoading = ref(false);
const selectedTab = ref<MCPTabs>('workflows');
@@ -90,6 +93,25 @@ const onRemoveMCPAccess = async (workflow: WorkflowListItem) => {
}
};
const onUpdateDescription = (workflow: WorkflowListItem) => {
uiStore.openModalWithData({
name: WORKFLOW_DESCRIPTION_MODAL_KEY,
data: {
workflowId: workflow.id,
workflowDescription: workflow.description ?? '',
onSave: (updatedDescription: string | null) => {
const index = availableWorkflows.value.findIndex((w) => w.id === workflow.id);
if (index !== -1) {
availableWorkflows.value[index] = {
...availableWorkflows.value[index],
description: updatedDescription ?? undefined,
};
}
},
},
});
};
const onTableRefresh = async () => {
if (selectedTab.value === 'workflows') {
await fetchAvailableWorkflows();
@@ -196,6 +218,7 @@ const revokeClientAccess = async (client: OAuthClientResponseDto) => {
:workflows="availableWorkflows"
:loading="workflowsLoading"
@remove-mcp-access="onRemoveMCPAccess"
@update-description="onUpdateDescription"
@refresh="onRefreshWorkflows"
/>
<OAuthClientsTable
@@ -334,6 +334,72 @@ describe('WorkflowsTable', () => {
expect(emitted('removeMcpAccess')).toBeTruthy();
expect(emitted('removeMcpAccess')[0]).toEqual([workflow]);
});
it('should enable "Update Description" action when user has update permissions', async () => {
const workflow = createWorkflow({
scopes: ['workflow:read', 'workflow:update'],
});
const { getByTestId } = createComponent({
props: {
workflows: [workflow],
loading: false,
},
});
const actionToggle = getByTestId('mcp-workflow-action-toggle');
const toggleButton = within(actionToggle).getByRole('button');
await userEvent.click(toggleButton);
const menuItems = document.querySelectorAll('[data-test-id="action-updateDescription"]');
expect(menuItems.length).toBe(1);
expect(menuItems[0]).not.toHaveAttribute('disabled');
});
it('should disable "Update Description" action when user lacks update permissions', async () => {
const workflow = createWorkflow({
scopes: ['workflow:read'],
});
const { getByTestId } = createComponent({
props: {
workflows: [workflow],
loading: false,
},
});
const actionToggle = getByTestId('mcp-workflow-action-toggle');
const toggleButton = within(actionToggle).getByRole('button');
await userEvent.click(toggleButton);
const menuItems = document.querySelectorAll('[data-test-id="action-updateDescription"]');
expect(menuItems.length).toBe(1);
expect(menuItems[0]).toHaveClass('is-disabled');
});
it('should emit updateDescription event when action is clicked', async () => {
const workflow = createWorkflow({
scopes: ['workflow:read', 'workflow:update'],
});
const { getByTestId, emitted } = createComponent({
props: {
workflows: [workflow],
loading: false,
},
});
const actionToggle = getByTestId('mcp-workflow-action-toggle');
const toggleButton = within(actionToggle).getByRole('button');
await userEvent.click(toggleButton);
const menuItem = document.querySelector('[data-test-id="action-updateDescription"]');
expect(menuItem).not.toBeNull();
await userEvent.click(menuItem!);
expect(emitted('updateDescription')).toBeTruthy();
expect(emitted('updateDescription')[0]).toEqual([workflow]);
});
});
describe('Workflow links', () => {
@@ -27,6 +27,7 @@ const props = defineProps<Props>();
const emit = defineEmits<{
removeMcpAccess: [workflow: WorkflowListItem];
updateDescription: [workflow: WorkflowListItem];
}>();
const i18n = useI18n();
@@ -80,6 +81,11 @@ const getAvailableActions = (workflow: WorkflowListItem): Array<UserAction<Workf
value: 'removeFromMCP',
disabled: !permissions.workflow.update,
},
{
label: i18n.baseText('settings.mcp.workflows.table.action.updateDescription'),
value: 'updateDescription',
disabled: !permissions.workflow.update,
},
];
};
@@ -95,6 +101,9 @@ const onWorkflowAction = (action: string, workflow: WorkflowListItem) => {
case 'removeFromMCP':
emit('removeMcpAccess', workflow);
break;
case 'updateDescription':
emit('updateDescription', workflow);
break;
default:
break;
}
@@ -224,7 +233,11 @@ const navigateToWorkflowList = () => {
:show-after="MCP_TOOLTIP_DELAY"
:popper-class="$style['description-popper']"
>
<div :class="$style['description-cell']">
<div
data-test-id="mcp-workflow-description-cell"
:class="$style['description-cell']"
@click="emit('updateDescription', item)"
>
<span v-if="item.description">
<N8nText data-test-id="mcp-workflow-description">
{{ item.description }}
@@ -319,6 +332,11 @@ const navigateToWorkflowList = () => {
overflow: hidden;
color: var(--color--text);
padding: var(--spacing--2xs) 0;
cursor: pointer;
&:hover span {
color: var(--color--text--shade-1);
}
span {
display: flex;