From 29779f0c5ead117d080ec18a2eb2d4c41aada4b1 Mon Sep 17 00:00:00 2001 From: Kai Date: Wed, 19 Aug 2026 07:46:44 +0000 Subject: [PATCH] feat(editor): Show diff for closed review requests (#36539) --- .../frontend/@n8n/i18n/src/locales/en.json | 8 +- .../WorkflowReviewChangesSection.test.ts | 82 ++++++++++++++++++- .../WorkflowReviewChangesSection.vue | 76 ++++++++++++++--- .../WorkflowReviewDetailTabs.test.ts | 24 +++--- .../components/WorkflowReviewDetailTabs.vue | 29 ++++--- .../views/WorkflowReviewRequestsView.test.ts | 2 +- 6 files changed, 181 insertions(+), 40 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 977421d42c9..962ad13956f 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -5561,14 +5561,18 @@ "workflowReviews.detail.metadata.reviewers": "Reviewers", "workflowReviews.detail.metadata.noReviewers": "No reviewers assigned.", "workflowReviews.detail.metadata.workflow": "Workflow", - "workflowReviews.changes.closed.body": "Changes aren't shown for closed reviews.", - "workflowReviews.changes.empty": "This review doesn't contain any workflows.", + "workflowReviews.changes.workflowUnavailable": "The workflow in this review is no longer available.", "workflowReviews.changes.unavailable": "Couldn't load the changes for this review. Refresh to try again.", "workflowReviews.changes.versionUnavailable.body": "The submitted workflow version is no longer available.", + "workflowReviews.changes.closedWithoutApproval": "This review was closed without approval, so there are no changes to compare.", "workflowReviews.changes.noChanges": "No changes compared to the published version.", "workflowReviews.changes.sourceLabel": "Published: {version}", "workflowReviews.changes.targetLabel": "In review: {version}", "workflowReviews.changes.firstPublish.sourceEmpty": "This workflow hasn't been published before.", + "workflowReviews.changes.closed.noChanges": "No changes compared to the previously published version.", + "workflowReviews.changes.closed.sourceLabel": "Previously published: {version}", + "workflowReviews.changes.closed.targetLabel": "Approved: {version}", + "workflowReviews.changes.closed.firstPublish.sourceEmpty": "This workflow hadn't been published yet.", "workflowReviews.detail.notFound.title": "Review not found", "workflowReviews.detail.notFound.body": "This review doesn't exist or you don't have access to it.", "workflowReviews.detail.decision.trigger": "Review", diff --git a/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.test.ts b/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.test.ts index 1f15984e64c..1edce985c48 100644 --- a/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.test.ts +++ b/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.test.ts @@ -1,4 +1,6 @@ import type { + WorkflowReviewRequestDecision, + WorkflowReviewRequestState, WorkflowReviewRequestWorkflowDetail, WorkflowReviewVersionSnapshot, } from '@n8n/api-types'; @@ -33,7 +35,16 @@ vi.mock('@/features/workflows/workflowDiff/WorkflowDiffView.vue', () => ({ }, })); -const renderComponent = createComponentRenderer(WorkflowReviewChangesSection); +const renderSection = createComponentRenderer(WorkflowReviewChangesSection); + +/** Defaults to an open review; closed cases pass `state` and `decision` themselves. */ +const renderComponent = (options: { + props: { + workflow: WorkflowReviewRequestWorkflowDetail; + state?: WorkflowReviewRequestState; + decision?: WorkflowReviewRequestDecision; + }; +}) => renderSection({ props: { state: 'open', decision: 'pending', ...options.props } }); function makeNode(overrides: Partial = {}): INode { return { @@ -242,6 +253,75 @@ describe('WorkflowReviewChangesSection', () => { expect(isReactive(targetWorkflow?.nodes[0])).toBe(false); }); + describe('closed reviews', () => { + const approved = { state: 'closed', decision: 'approved' } as const; + + it('labels the frozen baseline and the approved version', () => { + const { getByTestId } = renderComponent({ + props: { + ...approved, + workflow: makeWorkflow({ + pinnedVersion: makeSnapshot({ name: 'Version hotpink' }), + baselineVersion: makeSnapshot({ + versionId: '0f123890-0000-0000-0000-000000000000', + name: 'Version teal', + nodes: [makeNode({ id: 'node-removed', name: 'Removed node' }), makeNode()], + }), + }), + }, + }); + + expect(getByTestId('workflow-review-changes-source-label')).toHaveTextContent( + 'Previously published: Version teal', + ); + expect(getByTestId('workflow-review-changes-target-label')).toHaveTextContent( + 'Approved: Version hotpink', + ); + }); + + // Nothing was published when this was approved, so the baseline is empty by + // right — not missing. The diff still renders, sourceless. + it('renders a sourceless diff when nothing was published at approval', () => { + const { getByTestId, queryByTestId } = renderComponent({ + props: { ...approved, workflow: makeWorkflow({ baselineVersion: null }) }, + }); + + expect(getByTestId('workflow-diff-view-stub')).toBeInTheDocument(); + expect(diffViewProps[0].sourceWorkflow).toBeUndefined(); + expect( + queryByTestId('workflow-review-changes-closed-without-approval'), + ).not.toBeInTheDocument(); + }); + + it('phrases the no-changes state in the past tense', () => { + const snapshot = makeSnapshot(); + const { getByTestId } = renderComponent({ + props: { + ...approved, + workflow: makeWorkflow({ pinnedVersion: snapshot, baselineVersion: snapshot }), + }, + }); + + expect(getByTestId('workflow-review-changes-no-changes')).toHaveTextContent( + 'No changes compared to the previously published version.', + ); + }); + + // Auto-closed by archiving, transferring or deleting the workflow: approval + // is the only thing that freezes a baseline, so there is nothing to diff. + it.each(['pending', 'changes_requested'] as const)( + 'explains that a review closed with decision %s has no changes to compare', + (decision) => { + const { getByTestId, queryByTestId } = renderComponent({ + props: { state: 'closed', decision, workflow: makeWorkflow() }, + }); + + expect(getByTestId('workflow-review-changes-closed-without-approval')).toBeInTheDocument(); + expect(queryByTestId('workflow-diff-view-stub')).not.toBeInTheDocument(); + }, + ); + }); + it('renders the diff when versions differ only by an execution flag', () => { const { getByTestId, queryByTestId } = renderComponent({ props: { diff --git a/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.vue b/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.vue index de5f5cb0ceb..bca3b9b22c6 100644 --- a/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.vue +++ b/packages/frontend/editor-ui/src/features/workflow-reviews/components/WorkflowReviewChangesSection.vue @@ -1,5 +1,7 @@