diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 882df19c5ed..e9edf4ae7a4 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -3465,6 +3465,7 @@ "settings.context.skills.empty.description": "Add a skill by hand or upload a SKILL.md file. Agents in the scope of the skill can then use it.", "settings.context.skills.modal.scope.label": "Where can this skill be added?", "settings.context.skills.modal.scope.tooltip": "A global skill can be added to any agent. A project skill can be added to the agents of that project. An agent skill belongs to one agent.", + "settings.context.skills.modal.scope.noDelete": "You need permission to delete this skill to move it, because moving removes it from where it is now.", "settings.context.skills.modal.scope.inUse": "Used by {count} agent. Remove the skill from it to change where it is available. | Used by {count} agents. Remove the skill from them to change where it is available.", "settings.context.skills.delete.inUse": "Used by {count} agent. Remove the skill from the agent first. | Used by {count} agents. Remove the skill from each agent first.", "settings.context.skills.delete.confirm.title": "Delete skill?", diff --git a/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.test.ts b/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.test.ts index f90f6b4ae4a..3180690a9d3 100644 --- a/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.test.ts +++ b/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.test.ts @@ -335,6 +335,22 @@ describe('SettingsSkillsView', () => { ); }); + it('locks the scope when the user may edit but not delete the skill', async () => { + skillsStore.skills = [skill()]; + skillsStore.count = 1; + skillsStore.fetchSkill.mockResolvedValue(detail({ canEdit: true, canDelete: false })); + const { getByText } = renderView(); + await new Promise(process.nextTick); + + await userEvent.click(getByText('research-search')); + await new Promise(process.nextTick); + + expect(lastModalData().readOnly).toBe(false); + expect(lastModalData().scopeLockedReason).toBe( + 'You need permission to delete this skill to move it, because moving removes it from where it is now.', + ); + }); + it('moves an unused skill to the scope the user picks', async () => { skillsStore.skills = [skill()]; skillsStore.count = 1; diff --git a/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.vue b/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.vue index 26c1c94d7b2..73a53fbc232 100644 --- a/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.vue +++ b/packages/frontend/editor-ui/src/features/settings/context/views/SettingsSkillsView.vue @@ -245,6 +245,28 @@ function scopeValueOf(skill: Pick): string return skill.scope; } +/** + * Why the scope of a saved skill cannot change, if it cannot. The backend refuses a move + * while an agent uses the skill, and a move needs the delete permission where the skill is, + * because it takes the skill out of that scope. + */ +function scopeLock( + detail: Pick, +): Pick { + if (detail.usedByAgents > 0) { + return { + scopeLockedReason: i18n.baseText('settings.context.skills.modal.scope.inUse', { + interpolate: { count: detail.usedByAgents }, + adjustToNumber: detail.usedByAgents, + }), + }; + } + if (!detail.canDelete) { + return { scopeLockedReason: i18n.baseText('settings.context.skills.modal.scope.noDelete') }; + } + return {}; +} + async function openEditModal(item: Pick) { try { const detail = await skillsStore.fetchSkill(item.id); @@ -254,22 +276,13 @@ async function openEditModal(item: Pick) { const options = scopeOptions.value.some((option) => option.value === currentValue) ? scopeOptions.value : [{ value: currentValue, ...scopeDisplay(detail) }, ...scopeOptions.value]; - // The backend refuses to move a skill while an agent uses it. - const inUse = detail.usedByAgents > 0; openModal({ skill: detail.skill, skillId: detail.id, readOnly: !detail.canEdit, scopeOptions: options, initialScope: currentValue, - ...(inUse - ? { - scopeLockedReason: i18n.baseText('settings.context.skills.modal.scope.inUse', { - interpolate: { count: detail.usedByAgents }, - adjustToNumber: detail.usedByAgents, - }), - } - : {}), + ...scopeLock(detail), existingSkillNames: [], onConfirm: async ({ skill, scope }) => await onSave(