mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
feat(editor): Edit workflow descriptions from the MCP page (#22967)
This commit is contained in:
@@ -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
|
||||
|
||||
+66
@@ -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', () => {
|
||||
|
||||
+19
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user