From dde9871497120d0aa7b74e01aa9d95ff60df8836 Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Fri, 16 Oct 2026 08:18:13 +0000 Subject: [PATCH] feat(editor): Link to the skills settings page from the agent skill picker (no-changelog) (#41228) --- .../frontend/@n8n/i18n/src/locales/en.json | 1 + .../agents/__tests__/AgentSkillModal.test.ts | 43 +++++++++++++++++++ .../agents/components/AgentSkillModal.vue | 18 +++++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 4f69e277b05..882df19c5ed 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -8979,6 +8979,7 @@ "agents.builder.skills.picker.create": "Create skill", "agents.builder.skills.picker.create.description": "Write a new skill for this agent", "agents.builder.skills.picker.add": "Add skill", + "agents.builder.skills.picker.manage": "Manage skills", "agents.builder.skills.picker.loading": "Loading skills…", "agents.builder.skills.picker.empty": "No skills to add. Create one, or ask an admin to share one with this project.", "agents.builder.skills.picker.noResults": "No skills match your search", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSkillModal.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSkillModal.test.ts index 9772553e595..70b587a6188 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSkillModal.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentSkillModal.test.ts @@ -3,6 +3,7 @@ import { ref } from 'vue'; import { createComponentRenderer } from '@/__tests__/render'; import { createTestingPinia } from '@pinia/testing'; import { mockedStore } from '@/__tests__/utils'; +import { VIEWS } from '@/app/constants'; import { useUIStore } from '@/app/stores/ui.store'; import { configure, fireEvent, waitFor } from '@testing-library/vue'; import userEvent from '@testing-library/user-event'; @@ -75,6 +76,14 @@ vi.mock('@/features/shared/toolsConnection/ToolsConnectionModal.vue', () => ({ }, })); +const { resolveRoute } = vi.hoisted(() => ({ + resolveRoute: vi.fn(() => ({ href: '/settings/context/skills' })), +})); +vi.mock('vue-router', async (importOriginal) => ({ + ...(await importOriginal()), + useRouter: () => ({ resolve: resolveRoute }), +})); + vi.mock('../composables/useAgentTelemetry', () => ({ useAgentTelemetry: () => ({ trackImportedSkill }), })); @@ -667,6 +676,40 @@ describe('AgentSkillModal', () => { ); }); + it('links to the skills settings page from the picker, in a new tab', async () => { + listAttachableSkills.mockResolvedValue([projectSkill]); + const open = vi.spyOn(window, 'open').mockReturnValue(null); + const { getByTestId, queryByTestId } = renderModal({ + hubSkillsEnabled: true, + onAttach: vi.fn(), + }); + + expect(getByTestId('agent-skill-picker-manage')).toHaveTextContent( + 'agents.builder.skills.picker.manage', + ); + // The picker step has no Cancel and no Save. + expect(queryByTestId('agent-modal-cancel')).not.toBeInTheDocument(); + expect(queryByTestId('agent-skill-create-save')).not.toBeInTheDocument(); + + await fireEvent.click(getByTestId('agent-skill-picker-manage')); + + expect(resolveRoute).toHaveBeenCalledWith({ name: VIEWS.SETTINGS_CONTEXT_SKILLS }); + expect(open).toHaveBeenCalledWith('/settings/context/skills', '_blank', 'noopener'); + open.mockRestore(); + }); + + it('shows the settings link only on the picker step', async () => { + listAttachableSkills.mockResolvedValue([projectSkill]); + const { getByTestId, queryByTestId } = renderModal({ + hubSkillsEnabled: true, + onAttach: vi.fn(), + }); + + await fireEvent.click(getByTestId('agent-skill-picker-create')); + + expect(queryByTestId('agent-skill-picker-manage')).not.toBeInTheDocument(); + }); + it('goes from the picker to the upload step and back', async () => { listAttachableSkills.mockResolvedValue([projectSkill]); const { getByTestId, queryByTestId } = renderModal({ diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentSkillModal.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentSkillModal.vue index 3311ee752b1..db2dd3c289b 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentSkillModal.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentSkillModal.vue @@ -2,6 +2,7 @@ import cloneDeep from 'lodash/cloneDeep'; import isEqual from 'lodash/isEqual'; import { computed, nextTick, onMounted, ref, useTemplateRef, watch } from 'vue'; +import { useRouter } from 'vue-router'; import { AGENT_SKILL_INSTRUCTIONS_MAX_LENGTH, AGENT_SKILL_REFERENCE_MAX_COUNT, @@ -25,6 +26,7 @@ import type { IconName } from '@n8n/design-system'; import { useI18n, type BaseTextKey } from '@n8n/i18n'; import { useRootStore } from '@n8n/stores/useRootStore'; +import { VIEWS } from '@/app/constants'; import { useUIStore } from '@/app/stores/ui.store'; import ToolsConnectionModal from '@/features/shared/toolsConnection/ToolsConnectionModal.vue'; import type { @@ -104,6 +106,7 @@ const props = defineProps<{ const i18n = useI18n(); const uiStore = useUIStore(); +const router = useRouter(); const rootStore = useRootStore(); const { showError } = useToast(); const agentTelemetry = useAgentTelemetry(); @@ -231,6 +234,7 @@ const showRemove = computed(() => { return step.value === 'preview' ? previewIsAdded.value : isEditing.value; }); const footerPrimaryAction = computed<'add' | 'save' | 'useLatest' | undefined>(() => { + if (step.value === 'select') return undefined; if (step.value === 'preview') return previewIsAdded.value ? undefined : 'add'; if (isOutdated.value) return props.data.onUseLatestVersion ? 'useLatest' : undefined; return isReadOnly.value ? undefined : 'save'; @@ -469,6 +473,11 @@ async function attachLibrarySkill(skillId: string) { } } +/** Opens Settings > Context > Skills in a new tab, so the agent and the picker stay open. */ +function onManageSkills() { + window.open(router.resolve({ name: VIEWS.SETTINGS_CONTEXT_SKILLS }).href, '_blank', 'noopener'); +} + function onPreviewAdd() { if (!previewSkill.value) return; void attachLibrarySkill(previewSkill.value.id); @@ -651,7 +660,8 @@ function onRemove() { :title-error="visibleNameError" :editable-title="step === 'manual' || (step === 'edit' && !isReadOnly)" :show-back="step === 'manual' || step === 'preview' || (step === 'upload' && hasPicker)" - :show-footer="step === 'manual' || step === 'edit' || step === 'preview'" + :show-footer="step === 'select' || step === 'manual' || step === 'edit' || step === 'preview'" + :show-cancel="step !== 'select'" :body-flush="step !== 'select'" :busy="isImporting || previewLoading || isSaving" size="fit" @@ -846,6 +856,12 @@ function onRemove() { {{ removeLabel }} +