diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 128582bab44..dacf8af4f07 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -3056,7 +3056,7 @@ "workflowActivator.inactive": "Inactive", "workflowActivator.showError.title": "Workflow could not be {newStateName}", "workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.message": "Please resolve outstanding issues before publishing", - "workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title": "{count} node has issues, fix before publishing: | {count} nodes have issues, fix before publishing:", + "workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title": "{count} node has issues, fix them before publishing. | {count} nodes have issues, fix them before publishing.", "workflowActivator.showMessage.activeChangedWorkflowIdUndefined.message": "Please save it before activating", "workflowActivator.showMessage.activeChangedWorkflowIdUndefined.title": "Problem activating workflow", "workflowActivator.showMessage.displayActivationError.message.catchBlock": "Sorry there was a problem requesting the error", 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 daccf0428ad..06b41e522d0 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 @@ -84,10 +84,26 @@ const createMockActiveVersion = (versionId: string) => ({ description: null, }); +const triggerNode: INodeUi = { + id: 'trigger-1', + name: 'Webhook Trigger', + type: 'n8n-nodes-base.webhook', + typeVersion: 1, + position: [0, 0], + parameters: {}, + disabled: false, +}; + describe('WorkflowHeaderDraftPublishActions', () => { let workflowsStore: MockedStore; let uiStore: MockedStore; + const setupEnabledPublishButton = (overrides = {}) => { + workflowsStore.workflowTriggerNodes = [triggerNode]; + workflowsStore.nodesIssuesExist = false; + Object.assign(workflowsStore, overrides); + }; + beforeEach(() => { workflowsStore = mockedStore(useWorkflowsStore); uiStore = mockedStore(useUIStore); @@ -192,6 +208,13 @@ describe('WorkflowHeaderDraftPublishActions', () => { it('should open publish modal when clicked and workflow is saved', async () => { const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); uiStore.stateIsDirty = false; + setupEnabledPublishButton({ + workflow: { + ...workflowsStore.workflow, + versionId: 'version-1', + activeVersion: createMockActiveVersion('version-2'), + }, + }); const { getByTestId } = renderComponent(); @@ -207,6 +230,7 @@ describe('WorkflowHeaderDraftPublishActions', () => { it('should save workflow first when dirty then open publish modal', async () => { const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); uiStore.stateIsDirty = true; + setupEnabledPublishButton(); const { getByTestId } = renderComponent(); @@ -222,6 +246,7 @@ describe('WorkflowHeaderDraftPublishActions', () => { it('should save workflow first when isNewWorkflow is true then open publish modal', async () => { const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); uiStore.stateIsDirty = false; + setupEnabledPublishButton(); const { getByTestId } = renderComponent({ props: { @@ -243,6 +268,7 @@ describe('WorkflowHeaderDraftPublishActions', () => { const openModalSpy = vi.spyOn(uiStore, 'openModalWithData'); uiStore.stateIsDirty = true; mockSaveCurrentWorkflow.mockResolvedValue(false); + setupEnabledPublishButton(); const { getByTestId } = renderComponent(); @@ -253,18 +279,8 @@ describe('WorkflowHeaderDraftPublishActions', () => { }); }); - describe('Publish indicator visibility', () => { - const triggerNode: INodeUi = { - id: 'trigger-1', - name: 'Webhook Trigger', - type: 'n8n-nodes-base.webhook', - typeVersion: 1, - position: [0, 0], - parameters: {}, - disabled: false, - }; - - it('should not show publish indicator when there are no trigger nodes', () => { + describe('Publish button state', () => { + it('should show publish button disabled when there are no trigger nodes', () => { workflowsStore.workflowTriggerNodes = []; workflowsStore.workflow.versionId = 'version-1'; workflowsStore.workflow.activeVersion = createMockActiveVersion('version-2'); @@ -272,21 +288,21 @@ describe('WorkflowHeaderDraftPublishActions', () => { const { queryByTestId } = renderComponent(); - expect(queryByTestId('workflow-publish-indicator')).not.toBeInTheDocument(); + expect(queryByTestId('workflow-open-publish-modal-button')).toBeDisabled(); }); - it('should not show publish indicator when trigger node is disabled', () => { + it('should show publish button disabled when trigger node is disabled', () => { workflowsStore.workflowTriggerNodes = [{ ...triggerNode, disabled: true }]; workflowsStore.workflow.versionId = 'version-1'; workflowsStore.workflow.activeVersion = createMockActiveVersion('version-2'); uiStore.stateIsDirty = true; - const { queryByTestId } = renderComponent(); + const { getByTestId } = renderComponent(); - expect(queryByTestId('workflow-publish-indicator')).not.toBeInTheDocument(); + expect(getByTestId('workflow-open-publish-modal-button')).toBeDisabled(); }); - it('should show publish indicator when there are unpublished changes (versionId mismatch)', () => { + it('should show publish button enabled when there are unpublished changes (versionId mismatch)', () => { workflowsStore.workflowTriggerNodes = [triggerNode]; workflowsStore.workflow.versionId = 'version-1'; workflowsStore.workflow.activeVersion = createMockActiveVersion('version-2'); @@ -294,10 +310,10 @@ describe('WorkflowHeaderDraftPublishActions', () => { const { getByTestId } = renderComponent(); - expect(getByTestId('workflow-publish-indicator')).toBeInTheDocument(); + expect(getByTestId('workflow-open-publish-modal-button')).not.toBeDisabled(); }); - it('should show publish indicator when state is dirty', () => { + it('should show publish button enabled when state is dirty', () => { workflowsStore.workflowTriggerNodes = [triggerNode]; workflowsStore.workflow.versionId = 'version-1'; workflowsStore.workflow.activeVersion = createMockActiveVersion('version-1'); @@ -305,10 +321,10 @@ describe('WorkflowHeaderDraftPublishActions', () => { const { getByTestId } = renderComponent(); - expect(getByTestId('workflow-publish-indicator')).toBeInTheDocument(); + expect(getByTestId('workflow-open-publish-modal-button')).not.toBeDisabled(); }); - it('should not show publish indicator when versions match and state is not dirty', () => { + it('should show publish button disabled when versions match and state is not dirty', () => { workflowsStore.workflowTriggerNodes = [triggerNode]; workflowsStore.workflow.versionId = 'version-1'; workflowsStore.workflow.activeVersion = createMockActiveVersion('version-1'); @@ -316,10 +332,10 @@ describe('WorkflowHeaderDraftPublishActions', () => { const { queryByTestId } = renderComponent(); - expect(queryByTestId('workflow-publish-indicator')).not.toBeInTheDocument(); + expect(queryByTestId('workflow-open-publish-modal-button')).toBeDisabled(); }); - it('should show publish indicator when workflow has never been published (no active version)', () => { + it('should show publish button enabled when workflow has never been published (no active version)', () => { workflowsStore.workflowTriggerNodes = [triggerNode]; workflowsStore.workflow.versionId = 'version-1'; workflowsStore.workflow.activeVersion = null; @@ -327,7 +343,7 @@ describe('WorkflowHeaderDraftPublishActions', () => { const { getByTestId } = renderComponent(); - expect(getByTestId('workflow-publish-indicator')).toBeInTheDocument(); + expect(getByTestId('workflow-open-publish-modal-button')).not.toBeDisabled(); }); }); 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 f8efa933ca7..9965b37dbf7 100644 --- a/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.vue +++ b/packages/frontend/editor-ui/src/app/components/MainHeader/WorkflowHeaderDraftPublishActions.vue @@ -84,7 +84,7 @@ const isWorkflowSaved = computed(() => { return !uiStore.stateIsDirty && !props.isNewWorkflow; }); -const showPublishIndicator = computed(() => { +const publishButtonEnabled = computed(() => { if (!containsTrigger.value) { return false; } @@ -100,6 +100,28 @@ const showPublishIndicator = computed(() => { ); }); +const publishTooltipText = computed(() => { + const wfHasAnyChanges = + workflowsStore.workflow.versionId !== workflowsStore.workflow.activeVersion?.versionId; + + if (!containsTrigger.value) { + return i18n.baseText('workflows.publishModal.noTriggerMessage'); + } + + if (workflowsStore.nodesIssuesExist) { + return i18n.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title', { + interpolate: { count: workflowsStore.nodesWithIssues.length }, + adjustToNumber: workflowsStore.nodesWithIssues.length, + }); + } + + if (!wfHasAnyChanges && !uiStore.stateIsDirty) { + return i18n.baseText('workflows.publishModal.noChanges'); + } + + return ''; +}); + const activeVersion = computed(() => workflowsStore.workflow.activeVersion); const activeVersionName = computed(() => { @@ -144,20 +166,20 @@ defineExpose({
- - {{ locale.baseText('workflows.publish') }} - - + + + + {{ locale.baseText('workflows.publish') }} + +
diff --git a/packages/testing/playwright/tests/e2e/regression/GHC-5776-ai-sessions-metadata-license-error.spec.ts b/packages/testing/playwright/tests/e2e/regression/GHC-5776-ai-sessions-metadata-license-error.spec.ts index 38e57f889e8..3f0d38707bc 100644 --- a/packages/testing/playwright/tests/e2e/regression/GHC-5776-ai-sessions-metadata-license-error.spec.ts +++ b/packages/testing/playwright/tests/e2e/regression/GHC-5776-ai-sessions-metadata-license-error.spec.ts @@ -15,7 +15,7 @@ test.describe('GHC-5776: AI sessions metadata should not cause console errors', await n8n.start.fromBlankCanvas(); // This is just to trigger the watch on the workflow id, which will trigger the fetch of the sessions metadata - await n8n.canvas.getOpenPublishModalButton().click(); + await n8n.canvas.getWorkflowSaveButton().click(); const consoleMessages = await n8n.page.consoleMessages(); const errorMessages = consoleMessages.filter((msg) => msg.type() === 'error');