From f69721ffeca1f55b7dcfc84771ff1a828c222ced Mon Sep 17 00:00:00 2001 From: Svetoslav Dekov Date: Tue, 2 Dec 2025 17:55:24 +0200 Subject: [PATCH] fix(editor): Fix displaying of workflow publish info (no-changelog) (#22605) --- .../backend-test-utils/src/db/workflows.ts | 11 ++++ .../shared-workflow.repository.ts | 2 +- .../cli/src/workflows/workflow.service.ts | 22 ++++++-- .../workflows/workflow.service.test.ts | 16 +++++- .../workflows/workflows.controller.test.ts | 12 ++++- .../WorkflowHeaderDraftPublishActions.test.ts | 52 +++++++++++++++++++ .../WorkflowHeaderDraftPublishActions.vue | 8 ++- .../workflowHistory/views/WorkflowHistory.vue | 16 ++++-- 8 files changed, 126 insertions(+), 13 deletions(-) diff --git a/packages/@n8n/backend-test-utils/src/db/workflows.ts b/packages/@n8n/backend-test-utils/src/db/workflows.ts index 0d8274f9ae6..40b47ff1d0c 100644 --- a/packages/@n8n/backend-test-utils/src/db/workflows.ts +++ b/packages/@n8n/backend-test-utils/src/db/workflows.ts @@ -6,6 +6,7 @@ import { SharedWorkflowRepository, WorkflowRepository, WorkflowHistoryRepository, + WorkflowPublishHistoryRepository, } from '@n8n/db'; import { Container } from '@n8n/di'; import type { WorkflowSharingRole } from '@n8n/permissions'; @@ -284,5 +285,15 @@ export async function createActiveWorkflow( await setActiveVersion(workflow.id, workflow.versionId); workflow.activeVersionId = workflow.versionId; + + if (userOrProject instanceof User) { + await Container.get(WorkflowPublishHistoryRepository).save({ + workflowId: workflow.id, + versionId: workflow.versionId, + event: 'activated', + userId: userOrProject.id, + }); + } + return workflow; } diff --git a/packages/@n8n/db/src/repositories/shared-workflow.repository.ts b/packages/@n8n/db/src/repositories/shared-workflow.repository.ts index ebe905aa9a9..3d7dca2dc69 100644 --- a/packages/@n8n/db/src/repositories/shared-workflow.repository.ts +++ b/packages/@n8n/db/src/repositories/shared-workflow.repository.ts @@ -171,7 +171,7 @@ export class SharedWorkflowRepository extends Repository { shared: { project: { projectRelations: { user: true } } }, tags: includeTags, parentFolder: includeParentFolder, - activeVersion: includeActiveVersion, + activeVersion: includeActiveVersion ? { workflowPublishHistory: true } : false, }, }, }); diff --git a/packages/cli/src/workflows/workflow.service.ts b/packages/cli/src/workflows/workflow.service.ts index f949d25d2cf..39669089314 100644 --- a/packages/cli/src/workflows/workflow.service.ts +++ b/packages/cli/src/workflows/workflow.service.ts @@ -552,26 +552,26 @@ export class WorkflowService { updatedAt: workflow.updatedAt, }); - const updatedWorkflow = await this.workflowRepository.findOne({ + const workflowForActivation = await this.workflowRepository.findOne({ where: { id: workflowId }, relations: ['activeVersion'], }); - if (!updatedWorkflow) { + if (!workflowForActivation) { throw new NotFoundError(`Workflow with ID "${workflowId}" could not be found.`); } this.eventService.emit('workflow-activated', { user, workflowId, - workflow: updatedWorkflow, + workflow: workflowForActivation, publicApi, }); await this._addToActiveWorkflowManager( user, workflowId, - updatedWorkflow, + workflowForActivation, activationMode, publicApi, ); @@ -583,6 +583,20 @@ export class WorkflowService { await this.workflowHistoryService.updateVersion(versionToActivate, workflowId, updateFields); } + // Fetch workflow again with workflowPublishHistory after activation to include the new entry + const updatedWorkflow = await this.workflowRepository.findOne({ + where: { id: workflowId }, + relations: { + activeVersion: { + workflowPublishHistory: true, + }, + }, + }); + + if (!updatedWorkflow) { + throw new NotFoundError(`Workflow with ID "${workflowId}" could not be found.`); + } + return updatedWorkflow; } diff --git a/packages/cli/test/integration/workflows/workflow.service.test.ts b/packages/cli/test/integration/workflows/workflow.service.test.ts index aab59faf82a..8ec37ce43a4 100644 --- a/packages/cli/test/integration/workflows/workflow.service.test.ts +++ b/packages/cli/test/integration/workflows/workflow.service.test.ts @@ -22,9 +22,9 @@ import { createWorkflowHistoryItem } from '../shared/db/workflow-history'; let globalConfig: GlobalConfig; let workflowService: WorkflowService; +let workflowPublishHistoryRepository: WorkflowPublishHistoryRepository; const activeWorkflowManager = mockInstance(ActiveWorkflowManager); const workflowHistoryService = mockInstance(WorkflowHistoryService); -const workflowPublishHistoryRepository = mockInstance(WorkflowPublishHistoryRepository); mockInstance(MessageEventBus); mockInstance(Telemetry); @@ -32,6 +32,7 @@ beforeAll(async () => { await testDb.init(); globalConfig = Container.get(GlobalConfig); + workflowPublishHistoryRepository = Container.get(WorkflowPublishHistoryRepository); workflowService = new WorkflowService( mock(), Container.get(SharedWorkflowRepository), @@ -56,7 +57,7 @@ beforeAll(async () => { }); afterEach(async () => { - await testDb.truncate(['WorkflowEntity', 'WorkflowHistory']); + await testDb.truncate(['WorkflowEntity', 'WorkflowHistory', 'WorkflowPublishHistory']); jest.restoreAllMocks(); }); @@ -129,6 +130,12 @@ describe('activateWorkflow()', () => { expect(updatedWorkflow.active).toBe(true); expect(updatedWorkflow.activeVersionId).toBe(workflow.versionId); + expect(updatedWorkflow.activeVersion).toBeDefined(); + expect(updatedWorkflow.activeVersion?.workflowPublishHistory).toHaveLength(1); + expect(updatedWorkflow.activeVersion?.workflowPublishHistory[0]).toMatchObject({ + event: 'activated', + versionId: workflow.versionId, + }); expect(addRecordSpy).toBeCalledWith({ event: 'activated', workflowId: workflow.id, @@ -153,6 +160,11 @@ describe('activateWorkflow()', () => { expect(updatedWorkflow.active).toBe(true); expect(updatedWorkflow.activeVersionId).toBe(newVersionId); expect(updatedWorkflow.versionId).toBe(workflow.versionId); + expect(updatedWorkflow.activeVersion?.workflowPublishHistory).toHaveLength(1); + expect(updatedWorkflow.activeVersion?.workflowPublishHistory[0]).toMatchObject({ + event: 'activated', + versionId: newVersionId, + }); expect(addRecordSpy).toBeCalledWith({ event: 'activated', diff --git a/packages/cli/test/integration/workflows/workflows.controller.test.ts b/packages/cli/test/integration/workflows/workflows.controller.test.ts index 33eefd3559d..9f21c635c61 100644 --- a/packages/cli/test/integration/workflows/workflows.controller.test.ts +++ b/packages/cli/test/integration/workflows/workflows.controller.test.ts @@ -591,7 +591,7 @@ describe('GET /workflows/:workflowId', () => { }); }); - test('should return active version', async () => { + test('should return active version with workflowPublishHistory', async () => { const workflow = await createActiveWorkflow({}, owner); const response = await authOwnerAgent.get(`/workflows/${workflow.id}`).expect(200); @@ -603,6 +603,11 @@ describe('GET /workflows/:workflowId', () => { versionId: workflow.activeVersionId, workflowId: workflow.id, }); + expect(activeVersion.workflowPublishHistory).toHaveLength(1); + expect(activeVersion.workflowPublishHistory[0]).toMatchObject({ + event: 'activated', + versionId: workflow.activeVersionId, + }); }); test('should return parent folder', async () => { @@ -2755,6 +2760,11 @@ describe('POST /workflows/:workflowId/activate', () => { expect(data.id).toBe(workflow.id); expect(data.activeVersionId).toBe(newVersionId); expect(data.activeVersion.versionId).toBe(newVersionId); + expect(data.activeVersion.workflowPublishHistory).toHaveLength(1); + expect(data.activeVersion.workflowPublishHistory[0]).toMatchObject({ + event: 'activated', + versionId: newVersionId, + }); expect(addRecordSpy).toBeCalledWith({ event: 'activated', diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.test.ts b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.test.ts index a031443c600..daccf0428ad 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.test.ts +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.test.ts @@ -134,6 +134,58 @@ describe('WorkflowHeaderDraftPublishActions', () => { expect(getByTestId('workflow-active-version-indicator')).toBeInTheDocument(); }); + + it('should use latest activation date from workflowPublishHistory when available', () => { + const oldDate = '2024-01-01T00:00:00.000Z'; + const latestActivationDate = '2024-06-15T10:30:00.000Z'; + workflowsStore.workflow.activeVersion = { + ...createMockActiveVersion('active-version-1'), + createdAt: oldDate, + workflowPublishHistory: [ + { + id: 1, + createdAt: oldDate, + event: 'activated' as const, + userId: 'user-1', + versionId: 'active-version-1', + workflowId: '1', + }, + { + id: 2, + createdAt: '2024-03-01T00:00:00.000Z', + event: 'deactivated' as const, + userId: 'user-1', + versionId: 'active-version-1', + workflowId: '1', + }, + { + id: 3, + createdAt: latestActivationDate, + event: 'activated' as const, + userId: 'user-1', + versionId: 'active-version-1', + workflowId: '1', + }, + ], + }; + + const { getByTestId } = renderComponent({ + global: { + stubs: { + N8nTooltip: { + template: '
', + }, + TimeAgo: { + props: ['date'], + template: '
{{ date }}
', + }, + }, + }, + }); + + expect(getByTestId('workflow-active-version-indicator')).toBeInTheDocument(); + expect(getByTestId('time-ago-stub')).toHaveTextContent(latestActivationDate); + }); }); describe('Publish button behavior', () => { diff --git a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.vue b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.vue index 30c81620199..59865fb240b 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.vue +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.vue @@ -15,6 +15,7 @@ import TimeAgo from '@/app/components/TimeAgo.vue'; import { getActivatableTriggerNodes } from '@/app/utils/nodeTypesUtils'; import { useWorkflowSaving } from '@/app/composables/useWorkflowSaving'; import { useRouter } from 'vue-router'; +import { getLastPublishedByUser } from '@/features/workflows/workflowHistory/utils'; const props = defineProps<{ readOnly?: boolean; @@ -88,6 +89,11 @@ const showPublishIndicator = computed(() => { const activeVersion = computed(() => workflowsStore.workflow.activeVersion); +const latestPublishDate = computed(() => { + const latestPublish = getLastPublishedByUser(activeVersion.value?.workflowPublishHistory ?? []); + return latestPublish?.createdAt; +}); + defineExpose({ importFileRef, }); @@ -103,7 +109,7 @@ defineExpose({ diff --git a/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue b/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue index b1f88d775fc..969999bd5be 100644 --- a/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue +++ b/packages/frontend/editor-ui/src/features/workflows/workflowHistory/views/WorkflowHistory.vue @@ -279,13 +279,21 @@ const publishWorkflowVersion = (id: WorkflowVersionId, data: WorkflowHistoryActi const publishEventBus = createEventBus(); publishEventBus.once('publish', (publishData) => { - // Update the history list with the new name and description + // Refresh the active workflow to get the updated activeVersion with workflowPublishHistory + activeWorkflow.value = workflowsStore.getWorkflowById(workflowId.value); + + // Update the history list with the new name, description, and workflowPublishHistory const historyItem = workflowHistory.value.find( (item) => item.versionId === publishData.versionId, ); if (historyItem) { historyItem.name = publishData.name; historyItem.description = publishData.description; + // Update workflowPublishHistory from the store's activeVersion + if (activeWorkflow.value?.activeVersion?.workflowPublishHistory) { + historyItem.workflowPublishHistory = + activeWorkflow.value.activeVersion.workflowPublishHistory; + } } // Refresh the selected workflow version if it's the one that was published @@ -294,12 +302,12 @@ const publishWorkflowVersion = (id: WorkflowVersionId, data: WorkflowHistoryActi ...selectedWorkflowVersion.value, name: publishData.name, description: publishData.description, + workflowPublishHistory: + activeWorkflow.value?.activeVersion?.workflowPublishHistory ?? + selectedWorkflowVersion.value.workflowPublishHistory, }; } - // Refresh the active workflow to get the updated activeVersion - activeWorkflow.value = workflowsStore.getWorkflowById(workflowId.value); - sendTelemetry('User published version from history'); });