From 6a5aa7862ea38cbf75445b13993018aaff587c79 Mon Sep 17 00:00:00 2001 From: Anne Aguirre Date: Mon, 5 Oct 2026 10:06:20 +0000 Subject: [PATCH] feat(core): Indicate unpublished changes in agent resource list view (#40177) --- .../frontend/@n8n/i18n/src/locales/en.json | 1 + .../agents/__tests__/AgentCard.test.ts | 51 +++++++++++++++++++ .../features/agents/components/AgentCard.vue | 12 ++++- 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 965b8bc2721..3cd9fc81f06 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -8147,6 +8147,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 b230bfc30f5..1d6e9042d94 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 @@ -104,6 +104,10 @@ const STUBS = { template: '', props: ['theme', 'bold'], }, + PublicationIndicator: { + template: '{{ label }}', + props: ['label', 'variant'], + }, N8nActionToggle: { name: 'N8nActionToggle', template: @@ -345,4 +349,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 a5507d93731..cf7693aa2be 100644 --- a/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue +++ b/packages/frontend/editor-ui/src/features/agents/components/AgentCard.vue @@ -56,6 +56,10 @@ 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 isUntitled = computed(() => props.agent.name === locale.baseText('agents.new.defaultName')); const isMcpEnabled = computed( @@ -225,7 +229,13 @@ async function toggleMCPAccess(enabled: boolean) {