mirror of
https://github.com/n8n-io/n8n.git
synced 2026-09-24 23:22:38 +08:00
fix(editor): Disable publish button when no changes (#23137)
Co-authored-by: Nikhil Kuriakose <nikhilkuria@gmail.com>
This commit is contained in:
co-authored by
Nikhil Kuriakose
parent
2f329b38f9
commit
e391a2830b
@@ -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",
|
||||
|
||||
+40
-24
@@ -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<typeof useWorkflowsStore>;
|
||||
let uiStore: MockedStore<typeof useUIStore>;
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+37
-26
@@ -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({
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<div v-if="!isArchived && workflowPermissions.update" :class="$style.publishButtonWrapper">
|
||||
<N8nButton
|
||||
:loading="autoSaveForPublish"
|
||||
:disabled="isWorkflowSaving"
|
||||
type="secondary"
|
||||
data-test-id="workflow-open-publish-modal-button"
|
||||
@click="onPublishButtonClick"
|
||||
>
|
||||
{{ locale.baseText('workflows.publish') }}
|
||||
</N8nButton>
|
||||
<span
|
||||
v-if="showPublishIndicator"
|
||||
:class="$style.publishButtonIndicator"
|
||||
data-test-id="workflow-publish-indicator"
|
||||
></span>
|
||||
<N8nTooltip :disabled="!publishTooltipText">
|
||||
<template #content>
|
||||
{{ publishTooltipText }}
|
||||
</template>
|
||||
<N8nButton
|
||||
:loading="autoSaveForPublish"
|
||||
:disabled="!publishButtonEnabled || isWorkflowSaving"
|
||||
type="secondary"
|
||||
data-test-id="workflow-open-publish-modal-button"
|
||||
@click="onPublishButtonClick"
|
||||
>
|
||||
{{ locale.baseText('workflows.publish') }}
|
||||
</N8nButton>
|
||||
</N8nTooltip>
|
||||
</div>
|
||||
<SaveButton
|
||||
type="primary"
|
||||
@@ -209,15 +231,4 @@ defineExpose({
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.publishButtonIndicator {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: -2px;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
background-color: var(--color--primary);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 2px var(--color--background--light-3);
|
||||
}
|
||||
</style>
|
||||
|
||||
+1
-1
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user