Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into improve-permission-…
Browse files Browse the repository at this point in the history
…errors
  • Loading branch information
netroy committed Mar 11, 2025
2 parents 0668160 + b6d5092 commit 0300696
Show file tree
Hide file tree
Showing 12 changed files with 380 additions and 28 deletions.
1 change: 1 addition & 0 deletions packages/cli/src/commands/base-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ export abstract class BaseCommand extends Command {
}

async finally(error: Error | undefined) {
if (error?.message) this.logger.error(error.message);
if (inTest || this.id === 'start') return;
if (Db.connectionState.connected) {
await sleep(100); // give any in-flight query some time to finish
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/editor-ui/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ const logHiringBanner = () => {
const updateGridWidth = async () => {
await nextTick();
if (appGrid.value) {
uiStore.appGridWidth = appGrid.value.clientWidth;
const { width, height } = appGrid.value.getBoundingClientRect();
uiStore.appGridDimensions = { width, height };
}
};
Expand Down
48 changes: 29 additions & 19 deletions packages/frontend/editor-ui/src/components/NDVDraggablePanels.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { useNDVStore } from '@/stores/ndv.store';
import { ndvEventBus } from '@/event-bus';
import NDVFloatingNodes from '@/components/NDVFloatingNodes.vue';
import type { MainPanelType, XYPosition } from '@/Interface';
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
import { ref, onMounted, onBeforeUnmount, computed, watch, nextTick } from 'vue';
import { useUIStore } from '@/stores/ui.store';
import { useThrottleFn } from '@vueuse/core';
Expand Down Expand Up @@ -43,6 +43,7 @@ const props = defineProps<Props>();
const isDragging = ref<boolean>(false);
const initialized = ref<boolean>(false);
const containerWidth = ref<number>(uiStore.appGridDimensions.width);
const emit = defineEmits<{
init: [{ position: number }];
Expand Down Expand Up @@ -84,28 +85,37 @@ onBeforeUnmount(() => {
ndvEventBus.off('setPositionByName', setPositionByName);
});
const containerWidth = computed(() => uiStore.appGridWidth);
watch(containerWidth, (width) => {
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
watch(
() => uiStore.appGridDimensions,
async (dimensions) => {
const ndv = document.getElementById('ndv');
if (ndv) {
await nextTick();
const { width: ndvWidth } = ndv.getBoundingClientRect();
containerWidth.value = ndvWidth;
} else {
containerWidth.value = dimensions.width;
}
const minRelativeWidth = pxToRelativeWidth(MIN_PANEL_WIDTH);
const isBelowMinWidthMainPanel = mainPanelDimensions.value.relativeWidth < minRelativeWidth;
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
if (isBelowMinWidthMainPanel) {
setMainPanelWidth(minRelativeWidth);
}
// Prevent the panel resizing below MIN_PANEL_WIDTH while maintain position
if (isBelowMinWidthMainPanel) {
setMainPanelWidth(minRelativeWidth);
}
const isBelowMinLeft = minimumLeftPosition.value > mainPanelDimensions.value.relativeLeft;
const isMaxRight = maximumRightPosition.value > mainPanelDimensions.value.relativeRight;
const isBelowMinLeft = minimumLeftPosition.value > mainPanelDimensions.value.relativeLeft;
const isMaxRight = maximumRightPosition.value > mainPanelDimensions.value.relativeRight;
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
if (width > MIN_WINDOW_WIDTH && isBelowMinLeft && isMaxRight) {
setMainPanelWidth(minRelativeWidth);
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
}
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
if (dimensions.width > MIN_WINDOW_WIDTH && isBelowMinLeft && isMaxRight) {
setMainPanelWidth(minRelativeWidth);
setPositions(getInitialLeftPosition(mainPanelDimensions.value.relativeWidth));
}
setPositions(mainPanelDimensions.value.relativeLeft);
});
setPositions(mainPanelDimensions.value.relativeLeft);
},
);
const currentNodePaneType = computed((): MainPanelType => {
if (!hasInputSlot.value) return 'inputless';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,7 @@ onBeforeUnmount(() => {

<template>
<el-dialog
id="ndv"
:model-value="(!!activeNode || renaming) && !isActiveStickyNode"
:before-close="close"
:show-close="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ describe('useContextMenu', () => {
} as never);

vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
vi.spyOn(NodeHelpers, 'isExecutable').mockReturnValue(true);
});

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

it('should disable test step option for sub-nodes (AI tool nodes)', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const subNode = nodeFactory({ type: 'n8n-nodes-base.hackerNewsTool' });
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValue(subNode);
vi.spyOn(NodeHelpers, 'isExecutable').mockReturnValueOnce(false);
open(mockEvent, { source: 'node-right-click', nodeId: subNode.id });

expect(isOpen.value).toBe(true);
expect(actions.value.find((action) => action.id === 'execute')?.disabled).toBe(true);
expect(targetNodeIds.value).toEqual([subNode.id]);
});

it('should return the correct actions when right clicking a Node', () => {
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
const node = nodeFactory();
Expand Down Expand Up @@ -141,7 +154,6 @@ describe('useContextMenu', () => {
expect(actions.value).toMatchSnapshot();
expect(targetNodeIds.value).toEqual([sticky.id]);
});

it('should return the correct actions when right clicking a Node', () => {
vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(true);
const { open, isOpen, actions, targetNodeIds } = useContextMenu();
Expand Down
15 changes: 13 additions & 2 deletions packages/frontend/editor-ui/src/composables/useContextMenu.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { ActionDropdownItem, XYPosition } from '@/Interface';
import type { ActionDropdownItem, XYPosition, INodeUi } from '@/Interface';
import { NOT_DUPLICATABLE_NODE_TYPES, STICKY_NODE_TYPE } from '@/constants';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store';
import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { INode, INodeTypeDescription } from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow';
import { computed, ref, watch } from 'vue';
import { getMousePosition } from '../utils/nodeViewUtils';
import { useI18n } from './useI18n';
Expand Down Expand Up @@ -94,6 +95,16 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
position.value = [0, 0];
};

const isExecutable = (node: INodeUi) => {
const currentWorkflow = workflowsStore.getCurrentWorkflow();
const workflowNode = currentWorkflow.getNode(node.name) as INode;
const nodeType = nodeTypesStore.getNodeType(
workflowNode.type,
workflowNode.typeVersion,
) as INodeTypeDescription;
return NodeHelpers.isExecutable(currentWorkflow, workflowNode, nodeType);
};

const open = (event: MouseEvent, menuTarget: ContextMenuTarget) => {
event.stopPropagation();

Expand Down Expand Up @@ -228,7 +239,7 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
{
id: 'execute',
label: i18n.baseText('contextMenu.test'),
disabled: isReadOnly.value,
disabled: isReadOnly.value || !isExecutable(nodes[0]),
},
{
id: 'rename',
Expand Down
10 changes: 8 additions & 2 deletions packages/frontend/editor-ui/src/stores/assistant.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,14 +147,20 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
function openChat() {
chatWindowOpen.value = true;
chatMessages.value = chatMessages.value.map((msg) => ({ ...msg, read: true }));
uiStore.appGridWidth = window.innerWidth - chatWidth.value;
uiStore.appGridDimensions = {
...uiStore.appGridDimensions,
width: window.innerWidth - chatWidth.value,
};
}

function closeChat() {
chatWindowOpen.value = false;
// Looks smoother if we wait for slide animation to finish before updating the grid width
setTimeout(() => {
uiStore.appGridWidth = window.innerWidth;
uiStore.appGridDimensions = {
...uiStore.appGridDimensions,
width: window.innerWidth,
};
// If session has ended, reset the chat
if (isSessionEnded.value) {
resetAssistantChat();
Expand Down
4 changes: 2 additions & 2 deletions packages/frontend/editor-ui/src/stores/ui.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ export const useUIStore = defineStore(STORES.UI, () => {
const pendingNotificationsForViews = ref<{ [key in VIEWS]?: NotificationOptions[] }>({});
const processingExecutionResults = ref<boolean>(false);

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

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

return {
appGridWidth,
appGridDimensions,
appliedTheme,
contextBasedTranslationKeys,
getLastSelectedNode,
Expand Down
28 changes: 28 additions & 0 deletions packages/nodes-base/utils/sendAndWait/test/util.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,34 @@ describe('Send and Wait utils tests', () => {

expect(result.workflowData).toEqual([[{ json: { data: { 'test 1': 'test value' } } }]]);
});

it('should return noWebhookResponse if method GET and user-agent is bot', async () => {
mockWebhookFunctions.getRequestObject.mockReturnValue({
method: 'GET',
headers: {
'user-agent': 'Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)',
},
query: { approved: 'false' },
} as any);

const send = jest.fn();

mockWebhookFunctions.getResponseObject.mockReturnValue({
send,
} as any);

mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => {
const params: { [key: string]: any } = {
responseType: 'approval',
};
return params[parameterName];
});

const result = await sendAndWaitWebhook.call(mockWebhookFunctions);

expect(send).toHaveBeenCalledWith('');
expect(result).toEqual({ noWebhookResponse: true });
});
});
});

Expand Down
10 changes: 9 additions & 1 deletion packages/nodes-base/utils/sendAndWait/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import isbot from 'isbot';
import {
NodeOperationError,
SEND_AND_WAIT_OPERATION,
Expand Down Expand Up @@ -324,11 +325,18 @@ const getFormResponseCustomizations = (context: IWebhookFunctions) => {
export async function sendAndWaitWebhook(this: IWebhookFunctions) {
const method = this.getRequestObject().method;
const res = this.getResponseObject();
const req = this.getRequestObject();

const responseType = this.getNodeParameter('responseType', 'approval') as
| 'approval'
| 'freeText'
| 'customForm';

if (responseType === 'approval' && isbot(req.headers['user-agent'])) {
res.send('');
return { noWebhookResponse: true };
}

if (responseType === 'freeText') {
if (method === 'GET') {
const { formTitle, formDescription, buttonLabel } = getFormResponseCustomizations(this);
Expand Down Expand Up @@ -424,7 +432,7 @@ export async function sendAndWaitWebhook(this: IWebhookFunctions) {
}
}

const query = this.getRequestObject().query as { approved: 'false' | 'true' };
const query = req.query as { approved: 'false' | 'true' };
const approved = query.approved === 'true';
return {
webhookResponse: ACTION_RECORDED_PAGE,
Expand Down
10 changes: 10 additions & 0 deletions packages/workflow/src/NodeHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,3 +1718,13 @@ export function getVersionedNodeType(
}
return object;
}

export function isTriggerNode(nodeTypeData: INodeTypeDescription) {
return nodeTypeData.group.includes('trigger');
}

export function isExecutable(workflow: Workflow, node: INode, nodeTypeData: INodeTypeDescription) {
const outputs = getNodeOutputs(workflow, node, nodeTypeData);
const outputNames = getConnectionTypes(outputs);
return outputNames.includes(NodeConnectionType.Main) || isTriggerNode(nodeTypeData);
}
Loading

0 comments on commit 0300696

Please sign in to comment.