Skip to content

Commit 0300696

Browse files
committed
Merge remote-tracking branch 'origin/master' into improve-permission-errors
2 parents 0668160 + b6d5092 commit 0300696

File tree

12 files changed

+380
-28
lines changed

12 files changed

+380
-28
lines changed

packages/cli/src/commands/base-command.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export abstract class BaseCommand extends Command {
240240
}
241241

242242
async finally(error: Error | undefined) {
243+
if (error?.message) this.logger.error(error.message);
243244
if (inTest || this.id === 'start') return;
244245
if (Db.connectionState.connected) {
245246
await sleep(100); // give any in-flight query some time to finish

packages/frontend/editor-ui/src/App.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ const logHiringBanner = () => {
6161
const updateGridWidth = async () => {
6262
await nextTick();
6363
if (appGrid.value) {
64-
uiStore.appGridWidth = appGrid.value.clientWidth;
64+
const { width, height } = appGrid.value.getBoundingClientRect();
65+
uiStore.appGridDimensions = { width, height };
6566
}
6667
};
6768

packages/frontend/editor-ui/src/components/NDVDraggablePanels.vue

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useNDVStore } from '@/stores/ndv.store';
99
import { ndvEventBus } from '@/event-bus';
1010
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
1111
import type { MainPanelType, XYPosition } from '@/Interface';
12-
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
12+
import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
1313
import { useUIStore } from '@/stores/ui.store';
1414
import { useThrottleFn } from '@vueuse/core';
1515
@@ -43,6 +43,7 @@ const props = defineProps<Props>();
4343
4444
const isDragging = ref<boolean>(false);
4545
const initialized = ref<boolean>(false);
46+
const containerWidth = ref<number>(uiStore.appGridDimensions.width);
4647
4748
const emit = defineEmits<{
4849
init: [{ position: number }];
@@ -84,28 +85,37 @@ onBeforeUnmount(() => {
8485
ndvEventBus.off('setPositionByName', setPositionByName);
8586
});
8687
87-
const containerWidth = computed(() => uiStore.appGridWidth);
88-
89-
watch(containerWidth, (width) => {
90-
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
91-
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
88+
watch(
89+
() => uiStore.appGridDimensions,
90+
async (dimensions) => {
91+
const ndv = document.getElementById('ndv');
92+
if (ndv) {
93+
await nextTick();
94+
const { width: ndvWidth } = ndv.getBoundingClientRect();
95+
containerWidth.value = ndvWidth;
96+
} else {
97+
containerWidth.value = dimensions.width;
98+
}
99+
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
100+
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
92101
93-
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
94-
if (isBelowMinWidthMainPanel) {
95-
setMainPanelWidth(minRelativeWidth);
96-
}
102+
// Prevent the panel resizing below MIN_PANEL_WIDTH while maintain position
103+
if (isBelowMinWidthMainPanel) {
104+
setMainPanelWidth(minRelativeWidth);
105+
}
97106
98-
const isBelowMinLeft = minimumLeftPosition.value > mainPanelDimensions.value.relativeLeft;
99-
const isMaxRight = maximumRightPosition.value > mainPanelDimensions.value.relativeRight;
107+
const isBelowMinLeft = minimumLeftPosition.value > mainPanelDimensions.value.relativeLeft;
108+
const isMaxRight = maximumRightPosition.value > mainPanelDimensions.value.relativeRight;
100109
101-
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
102-
if (width > MIN_WINDOW_WIDTH && isBelowMinLeft && isMaxRight) {
103-
setMainPanelWidth(minRelativeWidth);
104-
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
105-
}
110+
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
111+
if (dimensions.width > MIN_WINDOW_WIDTH && isBelowMinLeft && isMaxRight) {
112+
setMainPanelWidth(minRelativeWidth);
113+
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
114+
}
106115
107-
setPositions(mainPanelDimensions.value.relativeLeft);
108-
});
116+
setPositions(mainPanelDimensions.value.relativeLeft);
117+
},
118+
);
109119
110120
const currentNodePaneType = computed((): MainPanelType => {
111121
if (!hasInputSlot.value) return 'inputless';

packages/frontend/editor-ui/src/components/NodeDetailsView.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,7 @@ onBeforeUnmount(() => {
706706

707707
<template>
708708
<el-dialog
709+
id="ndv"
709710
:model-value="(!!activeNode || renaming) && !isActiveStickyNode"
710711
:before-close="close"
711712
:show-close="false"

packages/frontend/editor-ui/src/composables/useContextMenu.test.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ describe('useContextMenu', () => {
4747
} as never);
4848

4949
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
50+
vi.spyOn(NodeHelpers, 'isExecutable').mockReturnValue(true);
5051
});
5152

5253
afterEach(() => {
@@ -106,6 +107,18 @@ describe('useContextMenu', () => {
106107
expect(targetNodeIds.value).toEqual([basicChain.id]);
107108
});
108109

110+
it('should disable test step option for sub-nodes (AI tool nodes)', () => {
111+
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
112+
const subNode = nodeFactory({ type: 'n8n-nodes-base.hackerNewsTool' });
113+
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(subNode);
114+
vi.spyOn(NodeHelpers, 'isExecutable').mockReturnValueOnce(false);
115+
open(mockEvent, { source: 'node-right-click', nodeId: subNode.id });
116+
117+
expect(isOpen.value).toBe(true);
118+
expect(actions.value.find((action) => action.id === 'execute')?.disabled).toBe(true);
119+
expect(targetNodeIds.value).toEqual([subNode.id]);
120+
});
121+
109122
it('should return the correct actions when right clicking a Node', () => {
110123
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
111124
const node = nodeFactory();
@@ -141,7 +154,6 @@ describe('useContextMenu', () => {
141154
expect(actions.value).toMatchSnapshot();
142155
expect(targetNodeIds.value).toEqual([sticky.id]);
143156
});
144-
145157
it('should return the correct actions when right clicking a Node', () => {
146158
vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(true);
147159
const { open, isOpen, actions, targetNodeIds } = useContextMenu();

packages/frontend/editor-ui/src/composables/useContextMenu.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
import type { ActionDropdownItem, XYPosition } from '@/Interface';
1+
import type { ActionDropdownItem, XYPosition, INodeUi } from '@/Interface';
22
import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
33
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
44
import { useSourceControlStore } from '@/stores/sourceControl.store';
55
import { useUIStore } from '@/stores/ui.store';
66
import { useWorkflowsStore } from '@/stores/workflows.store';
77
import type { INode, INodeTypeDescription } from 'n8n-workflow';
8+
import { NodeHelpers } from 'n8n-workflow';
89
import { computed, ref, watch } from 'vue';
910
import { getMousePosition } from '../utils/nodeViewUtils';
1011
import { useI18n } from './useI18n';
@@ -94,6 +95,16 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
9495
position.value = [0, 0];
9596
};
9697

98+
const isExecutable = (node: INodeUi) => {
99+
const currentWorkflow = workflowsStore.getCurrentWorkflow();
100+
const workflowNode = currentWorkflow.getNode(node.name) as INode;
101+
const nodeType = nodeTypesStore.getNodeType(
102+
workflowNode.type,
103+
workflowNode.typeVersion,
104+
) as INodeTypeDescription;
105+
return NodeHelpers.isExecutable(currentWorkflow, workflowNode, nodeType);
106+
};
107+
97108
const open = (event: MouseEvent, menuTarget: ContextMenuTarget) => {
98109
event.stopPropagation();
99110

@@ -228,7 +239,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
228239
{
229240
id: 'execute',
230241
label: i18n.baseText('contextMenu.test'),
231-
disabled: isReadOnly.value,
242+
disabled: isReadOnly.value || !isExecutable(nodes[0]),
232243
},
233244
{
234245
id: 'rename',

packages/frontend/editor-ui/src/stores/assistant.store.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,14 +147,20 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
147147
function openChat() {
148148
chatWindowOpen.value = true;
149149
chatMessages.value = chatMessages.value.map((msg) => ({ ...msg, read: true }));
150-
uiStore.appGridWidth = window.innerWidth - chatWidth.value;
150+
uiStore.appGridDimensions = {
151+
...uiStore.appGridDimensions,
152+
width: window.innerWidth - chatWidth.value,
153+
};
151154
}
152155

153156
function closeChat() {
154157
chatWindowOpen.value = false;
155158
// Looks smoother if we wait for slide animation to finish before updating the grid width
156159
setTimeout(() => {
157-
uiStore.appGridWidth = window.innerWidth;
160+
uiStore.appGridDimensions = {
161+
...uiStore.appGridDimensions,
162+
width: window.innerWidth,
163+
};
158164
// If session has ended, reset the chat
159165
if (isSessionEnded.value) {
160166
resetAssistantChat();

packages/frontend/editor-ui/src/stores/ui.store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
184184
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
185185
const processingExecutionResults = ref<boolean>(false);
186186

187-
const appGridWidth = ref<number>(0);
187+
const appGridDimensions = ref<{ width: number; height: number }>({ width: 0, height: 0 });
188188

189189
// Last interacted with - Canvas v2 specific
190190
const lastInteractedWithNodeConnection = ref<Connection | undefined>();
@@ -576,7 +576,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
576576
};
577577

578578
return {
579-
appGridWidth,
579+
appGridDimensions,
580580
appliedTheme,
581581
contextBasedTranslationKeys,
582582
getLastSelectedNode,

packages/nodes-base/utils/sendAndWait/test/util.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,34 @@ describe('Send and Wait utils tests', () => {
368368

369369
expect(result.workflowData).toEqual([[{ json: { data: { 'test 1': 'test value' } } }]]);
370370
});
371+
372+
it('should return noWebhookResponse if method GET and user-agent is bot', async () => {
373+
mockWebhookFunctions.getRequestObject.mockReturnValue({
374+
method: 'GET',
375+
headers: {
376+
'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
377+
},
378+
query: { approved: 'false' },
379+
} as any);
380+
381+
const send = jest.fn();
382+
383+
mockWebhookFunctions.getResponseObject.mockReturnValue({
384+
send,
385+
} as any);
386+
387+
mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
388+
const params: { [key: string]: any } = {
389+
responseType: 'approval',
390+
};
391+
return params[parameterName];
392+
});
393+
394+
const result = await sendAndWaitWebhook.call(mockWebhookFunctions);
395+
396+
expect(send).toHaveBeenCalledWith('');
397+
expect(result).toEqual({ noWebhookResponse: true });
398+
});
371399
});
372400
});
373401

packages/nodes-base/utils/sendAndWait/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import isbot from 'isbot';
12
import {
23
NodeOperationError,
34
SEND_AND_WAIT_OPERATION,
@@ -324,11 +325,18 @@ const getFormResponseCustomizations = (context: IWebhookFunctions) => {
324325
export async function sendAndWaitWebhook(this: IWebhookFunctions) {
325326
const method = this.getRequestObject().method;
326327
const res = this.getResponseObject();
328+
const req = this.getRequestObject();
329+
327330
const responseType = this.getNodeParameter('responseType', 'approval') as
328331
| 'approval'
329332
| 'freeText'
330333
| 'customForm';
331334

335+
if (responseType === 'approval' && isbot(req.headers['user-agent'])) {
336+
res.send('');
337+
return { noWebhookResponse: true };
338+
}
339+
332340
if (responseType === 'freeText') {
333341
if (method === 'GET') {
334342
const { formTitle, formDescription, buttonLabel } = getFormResponseCustomizations(this);
@@ -424,7 +432,7 @@ export async function sendAndWaitWebhook(this: IWebhookFunctions) {
424432
}
425433
}
426434

427-
const query = this.getRequestObject().query as { approved: 'false' | 'true' };
435+
const query = req.query as { approved: 'false' | 'true' };
428436
const approved = query.approved === 'true';
429437
return {
430438
webhookResponse: ACTION_RECORDED_PAGE,

packages/workflow/src/NodeHelpers.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1718,3 +1718,13 @@ export function getVersionedNodeType(
17181718
}
17191719
return object;
17201720
}
1721+
1722+
export function isTriggerNode(nodeTypeData: INodeTypeDescription) {
1723+
return nodeTypeData.group.includes('trigger');
1724+
}
1725+
1726+
export function isExecutable(workflow: Workflow, node: INode, nodeTypeData: INodeTypeDescription) {
1727+
const outputs = getNodeOutputs(workflow, node, nodeTypeData);
1728+
const outputNames = getConnectionTypes(outputs);
1729+
return outputNames.includes(NodeConnectionType.Main) || isTriggerNode(nodeTypeData);
1730+
}

0 commit comments

Comments
 (0)