mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(editor): Link to the skills settings page from the agent skill picker (no-changelog) (#41228)
This commit is contained in:
parent
cc83b5bd77
commit
dde9871497
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<typeof import('vue-router')>()),
|
||||
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({
|
||||
|
|
|
|||
|
|
@ -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 }}
|
||||
</N8nButton>
|
||||
</template>
|
||||
<template v-else-if="step === 'select'" #footerLeft>
|
||||
<N8nButton variant="ghost" data-testid="agent-skill-picker-manage" @click="onManageSkills">
|
||||
<template #icon><N8nIcon icon="settings" :size="16" /></template>
|
||||
{{ i18n.baseText('agents.builder.skills.picker.manage') }}
|
||||
</N8nButton>
|
||||
</template>
|
||||
<template v-if="footerHint" #footerBeforeCancel>
|
||||
<N8nText
|
||||
size="small"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user