fix(editor): Fix displaying of workflow publish info (no-changelog) (#22605)

This commit is contained in:
Svetoslav Dekov
2025-12-02 17:55:24 +02:00
committed by GitHub
parent b5c19e1b89
commit f69721ffec
8 changed files with 126 additions and 13 deletions
@@ -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;
}
@@ -171,7 +171,7 @@ export class SharedWorkflowRepository extends Repository<SharedWorkflow> {
shared: { project: { projectRelations: { user: true } } },
tags: includeTags,
parentFolder: includeParentFolder,
activeVersion: includeActiveVersion,
activeVersion: includeActiveVersion ? { workflowPublishHistory: true } : false,
},
},
});
+18 -4
View File
@@ -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;
}
@@ -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',
@@ -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',
@@ -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: '<div><slot name="content" /></div>',
},
TimeAgo: {
props: ['date'],
template: '<div data-test-id="time-ago-stub">{{ date }}</div>',
},
},
},
});
expect(getByTestId('workflow-active-version-indicator')).toBeInTheDocument();
expect(getByTestId('time-ago-stub')).toHaveTextContent(latestActivationDate);
});
});
describe('Publish button behavior', () => {
@@ -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({
<N8nTooltip>
<template #content>
{{ activeVersion.name }}<br />{{ i18n.baseText('workflowHistory.item.active') }}
<TimeAgo :date="activeVersion.createdAt" />
<TimeAgo v-if="latestPublishDate" :date="latestPublishDate" />
</template>
<N8nIcon icon="circle-check" color="success" size="xlarge" :class="$style.icon" />
</N8nTooltip>
@@ -279,13 +279,21 @@ const publishWorkflowVersion = (id: WorkflowVersionId, data: WorkflowHistoryActi
const publishEventBus = createEventBus<WorkflowHistoryPublishModalEventBusEvents>();
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');
});