mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-17 05:29:44 +02:00
fix(editor): Allow copy and cut of a selection with a restricted node (no-changelog) (#41276)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f92b4c11d6
commit
dfec6dee81
|
|
@ -5143,26 +5143,24 @@ describe('useCanvasOperations', () => {
|
|||
expect(copiedData.nodeGroups).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not copy a selection that contains a restricted node type', async () => {
|
||||
it('should copy a selection that contains a restricted node type', async () => {
|
||||
const toast = useToast();
|
||||
const nodes = buildImportNodes();
|
||||
nodes[1].type = 'n8n-nodes-base.slack';
|
||||
workflowDocumentStoreInstance.allNodes = nodes;
|
||||
vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
|
||||
vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
|
||||
mockedStore(useTypeAvailabilityPoliciesStore).getNodeTypeAvailability.mockImplementation(
|
||||
(name) => ({ name, available: name !== 'n8n-nodes-base.slack' }),
|
||||
);
|
||||
|
||||
const { copyNodes } = useCanvasOperations();
|
||||
const copied = await copyNodes(['1', '2']);
|
||||
await copyNodes(['1', '2']);
|
||||
|
||||
expect(copied).toBe(false);
|
||||
expect(useClipboard().copy).not.toHaveBeenCalled();
|
||||
expect(toast.showMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'warning',
|
||||
message: expect.stringContaining('n8n-nodes-base.slack'),
|
||||
}),
|
||||
const copiedData = JSON.parse(vi.mocked(useClipboard().copy).mock.calls[0][0] as string);
|
||||
expect(copiedData.nodes.map((node: INodeUi) => node.type)).toContain('n8n-nodes-base.slack');
|
||||
expect(toast.showMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'warning' }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -5371,6 +5369,28 @@ describe('useCanvasOperations', () => {
|
|||
expect(vi.mocked(useClipboard().copy).mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should cut a selection that contains a restricted node type', async () => {
|
||||
const nodes = buildImportNodes();
|
||||
nodes[1].type = 'n8n-nodes-base.slack';
|
||||
workflowDocumentStoreInstance.allNodes = nodes;
|
||||
vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
|
||||
vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
|
||||
mockedStore(useTypeAvailabilityPoliciesStore).getNodeTypeAvailability.mockImplementation(
|
||||
(name) => ({ name, available: name !== 'n8n-nodes-base.slack' }),
|
||||
);
|
||||
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) =>
|
||||
nodes.find((node) => node.id === id),
|
||||
);
|
||||
vi.mocked(workflowDocumentStoreInstance.incomingConnectionsByNodeName).mockReturnValue({});
|
||||
const removeNodeByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'removeNodeById');
|
||||
|
||||
const { cutNodes } = useCanvasOperations();
|
||||
await cutNodes(['1', '2']);
|
||||
|
||||
expect(useClipboard().copy).toHaveBeenCalledTimes(1);
|
||||
expect(removeNodeByIdSpy).toHaveBeenCalledWith('2');
|
||||
});
|
||||
|
||||
it('does not restore an anchor when cutting an explicitly selected group', async () => {
|
||||
const nodeTypesStore = useNodeTypesStore();
|
||||
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
|
||||
|
|
|
|||
|
|
@ -3696,10 +3696,8 @@ export function useCanvasOperations() {
|
|||
return result.nodes?.map((node) => node.id).filter(isPresent) ?? [];
|
||||
}
|
||||
|
||||
async function copyNodes(ids: string[]): Promise<boolean> {
|
||||
async function copyNodes(ids: string[]) {
|
||||
const nodes = workflowDocumentStore.value.getNodesByIds(ids);
|
||||
if (refuseRestrictedNodes(nodes)) return false;
|
||||
|
||||
const workflowData = deepCopy(getNodesToSave(nodes));
|
||||
if (!emptyCanvasGroupsEnabled.value) {
|
||||
removeEmptyCanvasGroupsFromWorkflowData(workflowData, nodes);
|
||||
|
|
@ -3716,13 +3714,10 @@ export function useCanvasOperations() {
|
|||
node_types: workflowData.nodes.map((node) => node.type),
|
||||
workflow_id: workflowDocumentStore.value.workflowId,
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function cutNodes(ids: string[], deleteWholeGroupIds: string[] = []) {
|
||||
if (!(await copyNodes(ids))) return;
|
||||
|
||||
await copyNodes(ids);
|
||||
deleteNodes(ids, { deleteWholeGroupIds });
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1143,7 +1143,7 @@ describe('NodeView', () => {
|
|||
expect(copiedText[0]).toContain('"name": "Existing"');
|
||||
});
|
||||
|
||||
it('refuses to copy a restricted node and does not claim success', async () => {
|
||||
it('copies a restricted node and confirms the copy', async () => {
|
||||
vi.spyOn(useTypeAvailabilityPoliciesStore(), 'getNodeTypeAvailability').mockReturnValue({
|
||||
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||
available: false,
|
||||
|
|
@ -1158,15 +1158,10 @@ describe('NodeView', () => {
|
|||
);
|
||||
await deferred?.();
|
||||
|
||||
expect(copiedText).toEqual([]);
|
||||
expect(mockShowMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'warning',
|
||||
message: expect.stringContaining(MANUAL_TRIGGER_NODE_TYPE),
|
||||
}),
|
||||
);
|
||||
expect(copiedText).toHaveLength(1);
|
||||
expect(mockShowMessage).toHaveBeenCalledWith(expect.objectContaining({ type: 'success' }));
|
||||
expect(mockShowMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'success' }),
|
||||
expect.objectContaining({ type: 'warning' }),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -625,7 +625,7 @@ async function onCopyNodes(ids: string[]) {
|
|||
return;
|
||||
}
|
||||
|
||||
if (!(await copyNodes(ids))) return;
|
||||
await copyNodes(ids);
|
||||
|
||||
toast.showMessage({ title: i18n.baseText('generic.copiedToClipboard'), type: 'success' });
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user