diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index ecd19021806..7161fd37234 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -8038,6 +8038,7 @@ "agents.backToWorkflow": "Back to workflow", "agents.heading": "Agents", "agents.list.published": "Published", + "agents.list.changesToPublish": "Changes to publish", "agents.list.noDescription": "No description", "agents.list.updatedAt": "Updated {date}", "agents.list.updated": "Last updated", diff --git a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts index 09746c419a3..55a2e77df53 100644 --- a/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts +++ b/packages/frontend/editor-ui/src/features/agents/__tests__/AgentCard.test.ts @@ -102,6 +102,10 @@ const STUBS = { template: '', props: ['theme', 'bold'], }, + PublicationIndicator: { + template: '{{ label }}', + props: ['label', 'variant'], + }, N8nActionToggle: { name: 'N8nActionToggle', template: @@ -328,4 +332,51 @@ describe('AgentCard', () => { 'agents.list.actions.disableMCPAccess', ); }); + + describe('publication indicator', () => { + const findIndicator = (wrapper: Awaited>) => + wrapper.find('[data-test-id="agent-card-publish-indicator"]'); + + it('is hidden on an unpublished agent', async () => { + const wrapper = await renderComponent(createAgent({ activeVersionId: null })); + + expect(findIndicator(wrapper).exists()).toBe(false); + }); + + it('shows Published with a success dot when the draft matches the published version', async () => { + const wrapper = await renderComponent( + createAgent({ versionId: 'v1', activeVersionId: 'v1', activeVersion }), + ); + + const indicator = findIndicator(wrapper); + expect(indicator.text()).toBe('agents.list.published'); + expect(indicator.attributes('data-state')).toBe('published'); + expect(indicator.attributes('data-variant')).toBe('success'); + }); + + it('shows Changes to publish with a warning dot when the draft differs from the published version', async () => { + const wrapper = await renderComponent( + createAgent({ versionId: 'v2', activeVersionId: 'v1', activeVersion }), + ); + + const indicator = findIndicator(wrapper); + expect(indicator.text()).toBe('agents.list.changesToPublish'); + expect(indicator.attributes('data-state')).toBe('changes-to-publish'); + expect(indicator.attributes('data-variant')).toBe('warning'); + }); + + it('returns to Published after the agent is republished', async () => { + const wrapper = await renderComponent( + createAgent({ versionId: 'v2', activeVersionId: 'v1', activeVersion }), + ); + + await wrapper.setProps({ + agent: createAgent({ versionId: 'v2', activeVersionId: 'v2', activeVersion }), + }); + + const indicator = findIndicator(wrapper); + expect(indicator.text()).toBe('agents.list.published'); + expect(indicator.attributes('data-variant')).toBe('success'); + }); + }); }); diff --git a/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue b/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue index 8f1ffbba127..ddb627c753a 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue @@ -55,6 +55,11 @@ const { canCreate, canUpdate, canDelete, canPublish, canUnpublish } = useAgentPe const isPublished = computed(() => props.agent.activeVersionId !== null); +// Saving a draft bumps `versionId`, so a mismatch means unpublished edits. +const hasUnpublishedChanges = computed( + () => isPublished.value && props.agent.versionId !== props.agent.activeVersionId, +); + const isMcpEnabled = computed( () => settingsStore.isModuleActive('mcp') && !!settingsStore.moduleSettings.mcp?.mcpAccessEnabled, ); @@ -216,7 +221,13 @@ async function toggleMCPAccess(enabled: boolean) {