mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
feat(core): Disable keyboard shortcuts for save/publish/unpublish/create new in canvas-only mode (no-changelog) (#36001)
This commit is contained in:
parent
72235d32f9
commit
fd701761dc
|
|
@ -5,6 +5,7 @@ import MainHeader from '@/app/components/MainHeader/MainHeader.vue';
|
|||
import { useWorkflowsStore } from '@/app/stores/workflows.store';
|
||||
import { useSourceControlStore } from '@/features/integrations/sourceControl.ee/sourceControl.store';
|
||||
import { useCollaborationStore } from '@/features/collaboration/collaboration/collaboration.store';
|
||||
import { useSettingsStore } from '@n8n/stores/settings.store';
|
||||
import { STORES } from '@n8n/stores';
|
||||
import { WorkflowIdKey, WorkflowDocumentStoreKey } from '@/app/constants/injectionKeys';
|
||||
import { computed, shallowRef } from 'vue';
|
||||
|
|
@ -95,11 +96,14 @@ describe('MainHeader', () => {
|
|||
let workflowsStore: MockedStore<typeof useWorkflowsStore>;
|
||||
let sourceControlStore: MockedStore<typeof useSourceControlStore>;
|
||||
let collaborationStore: MockedStore<typeof useCollaborationStore>;
|
||||
let settingsStore: MockedStore<typeof useSettingsStore>;
|
||||
|
||||
beforeEach(() => {
|
||||
workflowsStore = mockedStore(useWorkflowsStore);
|
||||
sourceControlStore = mockedStore(useSourceControlStore);
|
||||
collaborationStore = mockedStore(useCollaborationStore);
|
||||
settingsStore = mockedStore(useSettingsStore);
|
||||
settingsStore.settings.canvasOnly = false;
|
||||
|
||||
workflowsStore.setWorkflowId('1');
|
||||
workflowDocumentStore.hydrate({
|
||||
|
|
@ -132,6 +136,16 @@ describe('MainHeader', () => {
|
|||
expect(workflowDetails).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Especially important because WorkflowHeaderDraftPublishActions.vue child component of MainHeader
|
||||
// registers keyboard shortcuts like Cmd+S for saving, Cmd+U for unpublish and Cmd+P for publish.
|
||||
it('should not mount WorkflowDetails when canvas-only mode is on', () => {
|
||||
settingsStore.settings.canvasOnly = true;
|
||||
|
||||
const { queryByTestId } = renderComponent();
|
||||
|
||||
expect(queryByTestId('workflow-details-stub')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Regression: the header renders before the workflow document store is set
|
||||
// (e.g. the blank-canvas boot window). It must not throw when no NDV store is
|
||||
// available — it uses injectNDVStoreIfProvided() and guards the access.
|
||||
|
|
|
|||
|
|
@ -259,18 +259,20 @@ async function onWorkflowDeactivated() {
|
|||
[$style['canvas-only']]: settingsStore.isCanvasOnly,
|
||||
}"
|
||||
>
|
||||
<div v-show="!hideMenuBar && !settingsStore.isCanvasOnly" :class="$style['top-menu']">
|
||||
<WorkflowDetails
|
||||
v-if="workflowName"
|
||||
:id="workflowId"
|
||||
:tags="workflowTags"
|
||||
:name="workflowName"
|
||||
:current-folder="parentFolderForBreadcrumbs"
|
||||
:is-archived="workflowIsArchived"
|
||||
:description="workflowDescription"
|
||||
@workflow:deactivated="onWorkflowDeactivated"
|
||||
/>
|
||||
</div>
|
||||
<template v-if="!settingsStore.isCanvasOnly">
|
||||
<div v-show="!hideMenuBar" :class="$style['top-menu']">
|
||||
<WorkflowDetails
|
||||
v-if="workflowName"
|
||||
:id="workflowId"
|
||||
:tags="workflowTags"
|
||||
:name="workflowName"
|
||||
:current-folder="parentFolderForBreadcrumbs"
|
||||
:is-archived="workflowIsArchived"
|
||||
:description="workflowDescription"
|
||||
@workflow:deactivated="onWorkflowDeactivated"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<TabBar
|
||||
v-if="onWorkflowPage"
|
||||
:items="tabBarItems"
|
||||
|
|
|
|||
|
|
@ -39,6 +39,9 @@ import { useUIStore } from '@/app/stores/ui.store';
|
|||
import { createTestNode } from '@/__tests__/mocks';
|
||||
import { MESSAGE_AN_AGENT_NODE_TYPE } from '@/app/constants/nodeTypes';
|
||||
import { useAgentNodeCanvasGeometryStore } from '@/features/agents/agentNodeCanvasGeometry.store';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { useSettingsStore } from '@n8n/stores/settings.store';
|
||||
import { defaultSettings } from '@/__tests__/defaults';
|
||||
|
||||
// Instantiates a store that derives the workflow id from the route. These tests run
|
||||
// without a router, so resolve the id directly.
|
||||
|
|
@ -1008,6 +1011,46 @@ describe('Canvas', () => {
|
|||
expect(emitted()['copy:nodes']).toEqual([[['node-1', 'node-2']]]);
|
||||
});
|
||||
|
||||
describe('shortcuts disabled in canvas-only mode', () => {
|
||||
const shortcuts = [
|
||||
{ name: 'save:workflow', event: { key: 's', ctrlKey: true, metaKey: true } },
|
||||
{
|
||||
name: 'create:workflow',
|
||||
event: { key: 'n', ctrlKey: true, metaKey: true, altKey: true },
|
||||
},
|
||||
];
|
||||
|
||||
const setCanvasOnly = (canvasOnly: boolean) => {
|
||||
const settingsStore = mockedStore(useSettingsStore);
|
||||
settingsStore.settings = { ...defaultSettings, canvasOnly };
|
||||
};
|
||||
|
||||
it.each(shortcuts)('emits `$name` when canvas-only mode is off', async ({ name, event }) => {
|
||||
setCanvasOnly(false);
|
||||
|
||||
const { container, emitted } = renderComponent();
|
||||
await waitFor(() => expect(container.querySelector('.vue-flow')).toBeInTheDocument());
|
||||
|
||||
await fireEvent.keyDown(document, event);
|
||||
|
||||
expect(emitted()[name]).toEqual([[]]);
|
||||
});
|
||||
|
||||
it.each(shortcuts)(
|
||||
'does not emit `$name` when canvas-only mode is on',
|
||||
async ({ name, event }) => {
|
||||
setCanvasOnly(true);
|
||||
|
||||
const { container, emitted } = renderComponent();
|
||||
await waitFor(() => expect(container.querySelector('.vue-flow')).toBeInTheDocument());
|
||||
|
||||
await fireEvent.keyDown(document, event);
|
||||
|
||||
expect(emitted()[name]).toBeUndefined();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit `update:nodes:position` event', async () => {
|
||||
const nodes = [createCanvasNodeElement()];
|
||||
const { container, emitted } = renderComponent({
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { useSelectionValidation } from '@/app/composables/useSelectionValidation
|
|||
import { useToast } from '@n8n/composables/useToast';
|
||||
import { useI18n } from '@n8n/i18n';
|
||||
import { useUsersStore } from '@n8n/stores/users.store';
|
||||
import { useSettingsStore } from '@n8n/stores/settings.store';
|
||||
import { injectWorkflowDocumentStore } from '@/app/stores/workflowDocument.store';
|
||||
import { NODE_CREATOR_SHORTCUT_COACHMARK_KEY } from '@/features/shared/nodeCreator/composables/useNodeCreatorShortcutCoachmark';
|
||||
import type { NodeCreatorOpenSource } from '@/Interface';
|
||||
|
|
@ -206,6 +207,7 @@ const props = withDefaults(
|
|||
|
||||
const { isMobileDevice, controlKeyCode } = useDeviceSupport();
|
||||
const usersStore = useUsersStore();
|
||||
const settingsStore = useSettingsStore();
|
||||
const workflowDocumentStore = injectWorkflowDocumentStore();
|
||||
const message = useMessage();
|
||||
const toast = useToast();
|
||||
|
|
@ -533,11 +535,17 @@ const keyMap = computed(() => {
|
|||
},
|
||||
shift_s: () => emit('create:sticky'),
|
||||
shift_f: () => emit('toggle:focus-panel'),
|
||||
ctrl_alt_n: () => emit('create:workflow'),
|
||||
ctrl_alt_n: {
|
||||
disabled: () => settingsStore.isCanvasOnly,
|
||||
run: () => emit('create:workflow'),
|
||||
},
|
||||
ctrl_enter: () => emit('run:workflow'),
|
||||
// override the default cmd+s which saves the page html as file
|
||||
// also triggers manual save when autosave is disabled
|
||||
ctrl_s: () => emit('save:workflow'),
|
||||
ctrl_s: {
|
||||
disabled: () => settingsStore.isCanvasOnly,
|
||||
run: () => emit('save:workflow'),
|
||||
},
|
||||
shift_alt_t: async () => await onTidyUp({ source: 'keyboard-shortcut' }),
|
||||
alt_x: emitWithSelectedNodes((ids) => emit('extract-workflow', ids)),
|
||||
c: () => emit('start-chat'),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user