From 7bdfee9001c059fb7148a0e1cabb3a31e6ebaa18 Mon Sep 17 00:00:00 2001 From: yehorkardash Date: Mon, 18 May 2026 12:19:24 +0300 Subject: [PATCH] feat(editor): Show Slack docs link for agent setup (no-changelog) (#30608) --- .../frontend/@n8n/i18n/src/locales/en.json | 1 + .../agents/__tests__/AgentBuilderView.test.ts | 26 - .../AgentIntegrationCredentialPickers.test.ts | 107 ++- .../components/AgentAddTriggerModal.vue | 181 +---- .../AgentIntegrationSettingsForm.vue | 24 +- .../components/AgentIntegrationsPanel.vue | 701 ------------------ .../components/AgentSlackSettingsForm.vue | 220 ++++++ 7 files changed, 293 insertions(+), 967 deletions(-) delete mode 100644 packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationsPanel.vue create mode 100644 packages/frontend/editor-ui/src/features/agents/components/AgentSlackSettingsForm.vue diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 30005c1dfef..01060531988 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -6140,6 +6140,7 @@ "agents.builder.addTrigger.slack.hideJson": "Hide JSON", "agents.builder.addTrigger.slack.copyManifest": "Copy manifest", "agents.builder.addTrigger.slack.manifestTitle": "Slack App Manifest", + "agents.builder.addTrigger.slack.docsCalloutLink": "See docs", "agents.builder.addTrigger.telegram.accessMode.label": "Access mode", "agents.builder.addTrigger.telegram.accessMode.private": "Private", "agents.builder.addTrigger.telegram.accessMode.public": "Public", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts index eac67a6352a..2809a35683d 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentBuilderView.test.ts @@ -364,12 +364,6 @@ const commonStubs = { props: ['skill', 'disabled', 'errors'], emits: ['update:skill'], }, - AgentIntegrationsPanel: { - name: 'AgentIntegrationsPanel', - template: '
', - props: ['projectId', 'agentId', 'agentName', 'focusType', 'onlyConnected'], - emits: ['update:connected-triggers', 'trigger-added'], - }, AgentSessionsListView: { name: 'AgentSessionsListView', template: '
', @@ -884,23 +878,3 @@ describe('AgentBuilderView — three-column shell', () => { ]); }); }); - -/* - * DROPPED SPECS (no UI entry point in PR1): - * - * 1. 'fires User edited agent config with part=name when the agent name is updated' - * — relied on AgentHomeContent emitting 'update:name'. AgentHomeContent is - * removed from the three-column shell; name editing has no new UI entry point - * in PR1. Re-add once a name-edit surface lands in the editor column. - * - * 2. 'fires User edited agent config with part=description when the description is updated' - * — relied on AgentHomeContent emitting 'update:description'. Same reason as above. - * - * 3. 'fires User edited agent config with part=triggers when the connected-triggers list changes' - * — relied on AgentSettingsSidebar emitting 'update:connected-triggers'. The - * integrations panel is deleted in PR1; triggers telemetry has no new entry point. - * Re-add when AgentIntegrationsPanel or equivalent lands. - * - * 4. 'does not fire User edited agent config when the connected-triggers list is unchanged' - * — same as above. - */ diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentIntegrationCredentialPickers.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentIntegrationCredentialPickers.test.ts index 40360c7d997..1c044755d60 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentIntegrationCredentialPickers.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentIntegrationCredentialPickers.test.ts @@ -3,7 +3,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'; import { mount, flushPromises } from '@vue/test-utils'; import AgentAddTriggerModal from '../components/AgentAddTriggerModal.vue'; -import AgentIntegrationsPanel from '../components/AgentIntegrationsPanel.vue'; import { clearAgentIntegrationStatusCache } from '../composables/useAgentIntegrationStatus'; const { @@ -220,48 +219,22 @@ describe('agent integration credential picker usage', () => { ); }); - it('uses the shared credential picker in the integrations panel', async () => { - const wrapper = mount(AgentIntegrationsPanel, { - props: { - projectId: 'project-1', - agentId: 'agent-1', - agentName: 'Agent', - isPublished: true, - focusType: 'slack', - }, - global: { stubs: globalStubs }, - }); - await flushPromises(); - - const picker = wrapper.find('[data-testid="agent-credential-select-stub"]'); - expect(picker.exists()).toBe(true); - expect(picker.attributes('data-test-id-prop')).toBe('slack-credential-select'); - expect(picker.attributes('data-can-create')).toBe('true'); - - await wrapper.find('[data-testid="stub-create-credential"]').trigger('click'); - - expect(openNewCredential).toHaveBeenCalledWith( - 'slackApi', - false, - false, - 'project-1', - undefined, - undefined, - undefined, - { hideAskAssistant: true }, - ); - }); - it('passes denied credential creation permission to the shared picker', async () => { projectState.currentProject = { id: 'project-1', name: 'Project', scopes: [] }; - const wrapper = mount(AgentIntegrationsPanel, { + const wrapper = mount(AgentAddTriggerModal, { props: { - projectId: 'project-1', - agentId: 'agent-1', - agentName: 'Agent', - isPublished: true, - focusType: 'slack', + modalName: 'agentAddTriggerModal', + data: { + projectId: 'project-1', + agentId: 'agent-1', + agentName: 'Agent', + isPublished: true, + initialTriggerType: 'slack', + connectedTriggers: [], + onConnectedTriggersChange: vi.fn(), + onTriggerAdded: vi.fn(), + }, }, global: { stubs: globalStubs }, }); @@ -273,13 +246,19 @@ describe('agent integration credential picker usage', () => { }); it('defaults Telegram setup to private mode and requires a user ID before connecting', async () => { - const wrapper = mount(AgentIntegrationsPanel, { + const wrapper = mount(AgentAddTriggerModal, { props: { - projectId: 'project-1', - agentId: 'agent-1', - agentName: 'Agent', - isPublished: true, - focusType: 'telegram', + modalName: 'agentAddTriggerModal', + data: { + projectId: 'project-1', + agentId: 'agent-1', + agentName: 'Agent', + isPublished: true, + initialTriggerType: 'telegram', + connectedTriggers: [], + onConnectedTriggersChange: vi.fn(), + onTriggerAdded: vi.fn(), + }, }, global: { stubs: globalStubs }, }); @@ -313,13 +292,19 @@ describe('agent integration credential picker usage', () => { integrations: [{ type: 'telegram', credentialId: 'cred-telegram' }], }); - const wrapper = mount(AgentIntegrationsPanel, { + const wrapper = mount(AgentAddTriggerModal, { props: { - projectId: 'project-1', - agentId: 'agent-1', - agentName: 'Agent', - isPublished: true, - focusType: 'telegram', + modalName: 'agentAddTriggerModal', + data: { + projectId: 'project-1', + agentId: 'agent-1', + agentName: 'Agent', + isPublished: true, + initialTriggerType: 'telegram', + connectedTriggers: [], + onConnectedTriggersChange: vi.fn(), + onTriggerAdded: vi.fn(), + }, }, global: { stubs: globalStubs }, }); @@ -342,13 +327,19 @@ describe('agent integration credential picker usage', () => { ], }); - const wrapper = mount(AgentIntegrationsPanel, { + const wrapper = mount(AgentAddTriggerModal, { props: { - projectId: 'project-1', - agentId: 'agent-1', - agentName: 'Agent', - isPublished: true, - focusType: 'telegram', + modalName: 'agentAddTriggerModal', + data: { + projectId: 'project-1', + agentId: 'agent-1', + agentName: 'Agent', + isPublished: true, + initialTriggerType: 'telegram', + connectedTriggers: [], + onConnectedTriggersChange: vi.fn(), + onTriggerAdded: vi.fn(), + }, }, global: { stubs: globalStubs }, }); @@ -357,6 +348,6 @@ describe('agent integration credential picker usage', () => { const userIds = wrapper.find('[data-testid="telegram-user-ids"]'); expect(userIds.exists()).toBe(true); expect(userIds.find('input').attributes('disabled')).toBeDefined(); - expect(wrapper.find('[data-testid="telegram-telegram-settings-save"]').exists()).toBe(false); + expect(wrapper.find('[data-testid="telegram-disconnect-button"]').exists()).toBe(true); }); }); diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentAddTriggerModal.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentAddTriggerModal.vue index a9c0519115b..19ba66fd5b1 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentAddTriggerModal.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentAddTriggerModal.vue @@ -87,7 +87,6 @@ const credentialIdsBeforeNew = ref>>({}); const pendingNewCredentialType = ref(null); const linearCopied = ref(false); -const manifestCopied = ref(false); const SCHEDULE_ICON: IconName = 'clock'; @@ -160,109 +159,6 @@ async function copyLinearWebhookUrl() { }, 2000); } -const oauthCallbackUrl = computed(() => { - const configured = (rootStore.OAuthCallbackUrls as { oauth2?: string }).oauth2 ?? ''; - if (!configured) return ''; - try { - // Preserve the configured path (which may include a custom rest endpoint - // or base path) but rebase onto `urlBaseWebhook` so the callback uses - // the publicly reachable host from `WEBHOOK_URL` instead of the local - // browser origin (which is `http://localhost:5678` in dev). - const parsed = new URL(configured); - const base = rootStore.urlBaseWebhook.replace(/\/$/, ''); - return `${base}${parsed.pathname}${parsed.search}`; - } catch { - return configured; - } -}); - -const DEFAULT_SLACK_APP_NAME = 'n8n Agent'; - -// Slack app names accept letters, digits, spaces, periods, hyphens and -// underscores (max 35 chars per Slack's submission guidelines). Strip anything -// else and fall back to a sensible default if the agent name is empty after -// sanitisation, so the manifest always validates on Slack's side. -function sanitiseSlackAppName(raw: string): string { - const cleaned = raw - .replace(/[^a-zA-Z0-9 ._-]/g, '') - .replace(/\s+/g, ' ') - .trim() - .slice(0, 35); - return cleaned.length > 0 ? cleaned : DEFAULT_SLACK_APP_NAME; -} - -const slackAppManifest = computed(() => { - const agentName = sanitiseSlackAppName(props.data.agentName); - return JSON.stringify( - { - display_information: { - name: agentName, - }, - features: { - app_home: { - home_tab_enabled: true, - messages_tab_enabled: false, - messages_tab_read_only_enabled: false, - }, - bot_user: { - display_name: agentName, - always_online: true, - }, - }, - oauth_config: { - redirect_urls: [oauthCallbackUrl.value], - scopes: { - bot: [ - 'app_mentions:read', - 'assistant:write', - 'channels:history', - 'channels:join', - 'channels:manage', - 'channels:read', - 'chat:write', - 'chat:write.customize', - 'files:read', - 'files:write', - 'groups:read', - 'im:history', - 'im:read', - 'im:write', - 'mpim:read', - 'mpim:write', - 'search:read.public', - 'users:read', - 'users:read.email', - ], - }, - pkce_enabled: false, - }, - settings: { - event_subscriptions: { - request_url: webhookUrlFor('slack'), - bot_events: ['app_mention', 'assistant_thread_context_changed', 'message.im'], - }, - interactivity: { - is_enabled: true, - request_url: webhookUrlFor('slack'), - }, - org_deploy_enabled: false, - socket_mode_enabled: false, - token_rotation_enabled: false, - }, - }, - null, - 2, - ); -}); - -async function copyManifest() { - await navigator.clipboard.writeText(slackAppManifest.value); - manifestCopied.value = true; - setTimeout(() => { - manifestCopied.value = false; - }, 2000); -} - function computeConnectedTriggers(): string[] { return Object.keys(statuses.value) .filter((t) => statuses.value[t] === 'connected') @@ -579,6 +475,9 @@ onMounted(async () => { :disabled="isConnected(currentIntegration.type) || isLoading(currentIntegration.type)" :connected="isConnected(currentIntegration.type)" :saved-settings="integrationSettings[currentIntegration.type]" + :agent-name="data.agentName" + :project-id="data.projectId" + :agent-id="data.agentId" /> { >{{ i18n.baseText('agents.builder.addTrigger.editCredential') }} - - -
- - {{ i18n.baseText('agents.builder.addTrigger.slack.manifestTitle') }} - - - {{ i18n.baseText('agents.builder.addTrigger.slack.manifestHint') }} - -
- - - {{ - manifestCopied - ? i18n.baseText('agents.builder.addTrigger.copied') - : i18n.baseText('agents.builder.addTrigger.copy') - }} - -
{{ slackAppManifest }}
-
-
@@ -772,50 +641,6 @@ onMounted(async () => { gap: var(--spacing--sm); } -.manifestSection { - display: flex; - flex-direction: column; - gap: var(--spacing--2xs); -} - -.manifestHint { - color: var(--color--text--tint-1); -} - -.codeBlock { - position: relative; - margin-top: var(--spacing--3xs); -} - -/* Sit on top of the rounded container itself rather than inside the scrolling -
, so the button stays put as the user scrolls and never collides with
-   the scrollbar groove. The right offset clears typical macOS / overlay
-   scrollbars (~14px) plus our normal inner padding. */
-.codeBlockCopy {
-	position: absolute;
-	top: var(--spacing--2xs);
-	right: var(--spacing--lg);
-	z-index: 1;
-}
-
-.manifestCode {
-	margin: 0;
-	padding: var(--spacing--xs);
-	padding-right: calc(var(--spacing--2xl) + var(--spacing--lg));
-	background-color: var(--color--foreground--tint-2);
-	border-radius: var(--radius);
-	font-size: var(--font-size--2xs);
-	line-height: var(--line-height--xl);
-	overflow-x: auto;
-	max-height: 240px;
-	overflow-y: auto;
-	scrollbar-width: thin;
-	scrollbar-color: var(--border-color) transparent;
-	white-space: pre;
-	font-family: monospace;
-	color: var(--color--text);
-}
-
 .errorText {
 	color: var(--color--danger);
 }
diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationSettingsForm.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationSettingsForm.vue
index 9fcafdf0465..f3ef907b699 100644
--- a/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationSettingsForm.vue
+++ b/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationSettingsForm.vue
@@ -4,6 +4,7 @@ import type { AgentIntegrationSettings, AgentTelegramIntegrationSettings } from
 
 import { resolveSavedTelegramSettings } from '../utils/telegramAccessSettings';
 import AgentTelegramAccessSettingsForm from './AgentTelegramAccessSettingsForm.vue';
+import AgentSlackSettingsForm from './AgentSlackSettingsForm.vue';
 
 const props = withDefaults(
 	defineProps<{
@@ -11,8 +12,18 @@ const props = withDefaults(
 		disabled?: boolean;
 		connected?: boolean;
 		savedSettings?: AgentIntegrationSettings;
+		agentName?: string;
+		projectId?: string;
+		agentId?: string;
 	}>(),
-	{ disabled: false, connected: false, savedSettings: undefined },
+	{
+		disabled: false,
+		connected: false,
+		savedSettings: undefined,
+		agentName: '',
+		projectId: '',
+		agentId: '',
+	},
 );
 
 const telegramFormRef = ref>();
@@ -21,8 +32,6 @@ const telegramSavedSettings = computed props.type === 'telegram');
-
 const currentSettings = computed(
 	() => telegramFormRef.value?.currentSettings,
 );
@@ -45,9 +54,16 @@ defineExpose({ currentSettings, validationError, isDirty });
 
 
diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationsPanel.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationsPanel.vue
deleted file mode 100644
index 62b9e52e54a..00000000000
--- a/packages/frontend/editor-ui/src/features/agents/components/AgentIntegrationsPanel.vue
+++ /dev/null
@@ -1,701 +0,0 @@
-
-
-
-
-
diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentSlackSettingsForm.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentSlackSettingsForm.vue
new file mode 100644
index 00000000000..3994d8f7a0d
--- /dev/null
+++ b/packages/frontend/editor-ui/src/features/agents/components/AgentSlackSettingsForm.vue
@@ -0,0 +1,220 @@
+
+
+
+
+