Skip to content

Commit

Permalink
Merge branch 'cat-492-make-it-easier-to-start-developing-front-end-an…
Browse files Browse the repository at this point in the history
…d-e2e-tests' of github.com:n8n-io/n8n into cat-492-make-it-easier-to-start-developing-front-end-and-e2e-tests
  • Loading branch information
alexgrozav committed Jan 8, 2025
2 parents 694d5b3 + d5fdc91 commit 92cfadd
Show file tree
Hide file tree
Showing 24 changed files with 7,561 additions and 67 deletions.
19 changes: 17 additions & 2 deletions cypress/e2e/7-workflow-actions.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,9 +171,16 @@ describe('Workflow Actions', () => {
cy.get('#node-creator').should('not.exist');

WorkflowPage.actions.hitSelectAll();
cy.get('.jtk-drag-selected').should('have.length', 2);
WorkflowPage.actions.hitCopy();
successToast().should('exist');
// Both nodes should be copied
cy.window()
.its('navigator.clipboard')
.then((clip) => clip.readText())
.then((text) => {
const copiedWorkflow = JSON.parse(text);
expect(copiedWorkflow.nodes).to.have.length(2);
});
});

it('should paste nodes (both current and old node versions)', () => {
Expand Down Expand Up @@ -345,7 +352,15 @@ describe('Workflow Actions', () => {
WorkflowPage.actions.hitDeleteAllNodes();
WorkflowPage.getters.canvasNodes().should('have.length', 0);
// Button should be disabled
WorkflowPage.getters.executeWorkflowButton().should('be.disabled');
cy.ifCanvasVersion(
() => {
WorkflowPage.getters.executeWorkflowButton().should('be.disabled');
},
() => {
// In new canvas, button does not exist when there are no nodes
WorkflowPage.getters.executeWorkflowButton().should('not.exist');
},
);
// Keyboard shortcut should not work
WorkflowPage.actions.hitExecuteWorkflow();
successToast().should('not.exist');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,33 @@ describe('CanvasNode', () => {
expect(inputHandles.length).toBe(3);
expect(outputHandles.length).toBe(2);
});

it('should insert spacers after required non-main input handle', () => {
const { getAllByTestId } = renderComponent({
props: {
...createCanvasNodeProps({
data: {
inputs: [
{ type: NodeConnectionType.Main, index: 0 },
{ type: NodeConnectionType.AiAgent, index: 0, required: true },
{ type: NodeConnectionType.AiTool, index: 0 },
],
outputs: [],
},
}),
},
global: {
stubs: {
Handle: true,
},
},
});

const inputHandles = getAllByTestId('canvas-node-input-handle');

expect(inputHandles[1]).toHaveStyle('left: 20%');
expect(inputHandles[2]).toHaveStyle('left: 80%');
});
});

describe('toolbar', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ import { useContextMenu } from '@/composables/useContextMenu';
import type { NodeProps, XYPosition } from '@vue-flow/core';
import { Position } from '@vue-flow/core';
import { useCanvas } from '@/composables/useCanvas';
import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2';
import {
createCanvasConnectionHandleString,
insertSpacersBetweenEndpoints,
} from '@/utils/canvasUtilsV2';
import type { EventBus } from 'n8n-design-system';
import { createEventBus } from 'n8n-design-system';
import { isEqual } from 'lodash-es';
Expand Down Expand Up @@ -77,12 +80,18 @@ const nodeClasses = ref<string[]>([]);
const inputs = computed(() => props.data.inputs);
const outputs = computed(() => props.data.outputs);
const connections = computed(() => props.data.connections);
const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnection } =
useNodeConnections({
inputs,
outputs,
connections,
});
const {
mainInputs,
nonMainInputs,
requiredNonMainInputs,
mainOutputs,
nonMainOutputs,
isValidConnection,
} = useNodeConnections({
inputs,
outputs,
connections,
});
const isDisabled = computed(() => props.data.disabled);
Expand Down Expand Up @@ -114,23 +123,15 @@ function emitCanvasNodeEvent(event: CanvasEventBusEvents['nodes:action']) {
* Inputs
*/
const nonMainInputsWithSpacer = computed(() =>
insertSpacersBetweenEndpoints(nonMainInputs.value, requiredNonMainInputs.value.length),
);
const mappedInputs = computed(() => {
return [
...mainInputs.value.map(
createEndpointMappingFn({
mode: CanvasConnectionMode.Input,
position: Position.Left,
offsetAxis: 'top',
}),
),
...nonMainInputs.value.map(
createEndpointMappingFn({
mode: CanvasConnectionMode.Input,
position: Position.Bottom,
offsetAxis: 'left',
}),
),
];
...mainInputs.value.map(mainInputsMappingFn),
...nonMainInputsWithSpacer.value.map(nonMainInputsMappingFn),
].filter((endpoint) => !!endpoint);
});
/**
Expand All @@ -139,21 +140,9 @@ const mappedInputs = computed(() => {
const mappedOutputs = computed(() => {
return [
...mainOutputs.value.map(
createEndpointMappingFn({
mode: CanvasConnectionMode.Output,
position: Position.Right,
offsetAxis: 'top',
}),
),
...nonMainOutputs.value.map(
createEndpointMappingFn({
mode: CanvasConnectionMode.Output,
position: Position.Top,
offsetAxis: 'left',
}),
),
];
...mainOutputs.value.map(mainOutputsMappingFn),
...nonMainOutputs.value.map(nonMainOutputsMappingFn),
].filter((endpoint) => !!endpoint);
});
/**
Expand All @@ -179,10 +168,14 @@ const createEndpointMappingFn =
offsetAxis: 'top' | 'left';
}) =>
(
endpoint: CanvasConnectionPort,
endpoint: CanvasConnectionPort | null,
index: number,
endpoints: CanvasConnectionPort[],
): CanvasElementPortWithRenderData => {
endpoints: Array<CanvasConnectionPort | null>,
): CanvasElementPortWithRenderData | undefined => {
if (!endpoint) {
return;
}
const handleId = createCanvasConnectionHandleString({
mode,
type: endpoint.type,
Expand All @@ -207,6 +200,30 @@ const createEndpointMappingFn =
};
};
const mainInputsMappingFn = createEndpointMappingFn({
mode: CanvasConnectionMode.Input,
position: Position.Left,
offsetAxis: 'top',
});
const nonMainInputsMappingFn = createEndpointMappingFn({
mode: CanvasConnectionMode.Input,
position: Position.Bottom,
offsetAxis: 'left',
});
const mainOutputsMappingFn = createEndpointMappingFn({
mode: CanvasConnectionMode.Output,
position: Position.Right,
offsetAxis: 'top',
});
const nonMainOutputsMappingFn = createEndpointMappingFn({
mode: CanvasConnectionMode.Output,
position: Position.Top,
offsetAxis: 'left',
});
/**
* Events
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,12 @@ function openContextMenu(event: MouseEvent) {
var(--configurable-node--input-width)
);
justify-content: flex-start;
:global(.n8n-node-icon) {
margin-left: var(--configurable-node--icon-offset);
}
.description {
top: unset;
position: relative;
Expand Down
74 changes: 72 additions & 2 deletions packages/editor-ui/src/utils/canvasUtilsV2.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import {
checkOverlap,
createCanvasConnectionHandleString,
createCanvasConnectionId,
insertSpacersBetweenEndpoints,
mapCanvasConnectionToLegacyConnection,
mapLegacyConnectionsToCanvasConnections,
mapLegacyEndpointsToCanvasConnectionPort,
parseCanvasConnectionHandleString,
checkOverlap,
} from '@/utils/canvasUtilsV2';
import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import type { IConnections, INodeTypeDescription, IConnection } from 'n8n-workflow';
import type { CanvasConnection } from '@/types';
import { CanvasConnectionMode } from '@/types';
import type { INodeUi } from '@/Interface';
Expand Down Expand Up @@ -976,3 +977,72 @@ describe('checkOverlap', () => {
expect(checkOverlap(node1, node2)).toBe(false);
});
});

describe('insertSpacersBetweenEndpoints', () => {
it('should insert spacers when there are less than min endpoints count', () => {
const endpoints = [{ index: 0, required: true }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([{ index: 0, required: true }, null, null, null]);
});

it('should not insert spacers when there are at least min endpoints count', () => {
const endpoints = [{ index: 0, required: true }, { index: 1 }, { index: 2 }, { index: 3 }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual(endpoints);
});

it('should handle zero required endpoints', () => {
const endpoints = [{ index: 0, required: false }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([null, null, null, { index: 0, required: false }]);
});

it('should handle no endpoints', () => {
const endpoints: Array<{ index: number; required: boolean }> = [];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([null, null, null, null]);
});

it('should handle required endpoints greater than min endpoints count', () => {
const endpoints = [
{ index: 0, required: true },
{ index: 1, required: true },
{ index: 2, required: true },
{ index: 3, required: true },
{ index: 4, required: true },
];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual(endpoints);
});

it('should insert spacers between required and optional endpoints', () => {
const endpoints = [{ index: 0, required: true }, { index: 1, required: true }, { index: 2 }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([
{ index: 0, required: true },
{ index: 1, required: true },
null,
{ index: 2 },
]);
});

it('should handle required endpoints count greater than endpoints length', () => {
const endpoints = [{ index: 0, required: true }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4);
expect(result).toEqual([{ index: 0, required: true }, null, null, null]);
});

it('should handle min endpoints count less than required endpoints count', () => {
const endpoints = [{ index: 0, required: false }];
const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length;
const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 0);
expect(result).toEqual([{ index: 0, required: false }]);
});
});
20 changes: 20 additions & 0 deletions packages/editor-ui/src/utils/canvasUtilsV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,3 +210,23 @@ export function checkOverlap(node1: BoundingBox, node2: BoundingBox) {
)
);
}

export function insertSpacersBetweenEndpoints<T>(
endpoints: T[],
requiredEndpointsCount = 0,
minEndpointsCount = 4,
) {
const endpointsWithSpacers: Array<T | null> = [...endpoints];
const optionalNonMainInputsCount = endpointsWithSpacers.length - requiredEndpointsCount;
const spacerCount = minEndpointsCount - requiredEndpointsCount - optionalNonMainInputsCount;

// Insert `null` in between required non-main inputs and non-required non-main inputs
// to separate them visually if there are less than 4 inputs in total
if (endpointsWithSpacers.length < minEndpointsCount) {
for (let i = 0; i < spacerCount; i++) {
endpointsWithSpacers.splice(requiredEndpointsCount + i, 0, null);
}
}

return endpointsWithSpacers;
}
5 changes: 5 additions & 0 deletions packages/editor-ui/src/views/NodeView.v2.vue
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,11 @@ function onPinNodes(ids: string[], source: PinDataSource) {
}
async function onSaveWorkflow() {
const workflowIsSaved = !uiStore.stateIsDirty;
if (workflowIsSaved) {
return;
}
const saved = await workflowHelpers.saveCurrentWorkflow();
if (saved) {
canvasEventBus.emit('saved:workflow');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ICredentialType, INodeProperties, Icon } from 'n8n-workflow';
import type { ICredentialType, INodeProperties } from 'n8n-workflow';

export class MicrosoftEntraOAuth2Api implements ICredentialType {
name = 'microsoftEntraOAuth2Api';
Expand All @@ -7,17 +7,16 @@ export class MicrosoftEntraOAuth2Api implements ICredentialType {

extends = ['microsoftOAuth2Api'];

icon: Icon = 'file:icons/Azure.svg';

documentationUrl = 'microsoftentra';

properties: INodeProperties[] = [
{
displayName: 'Scope',
name: 'scope',
type: 'hidden',
// Sites.FullControl.All required to update user specific properties https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/1316
default:
'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All',
'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All User.ReadWrite.All Directory.AccessAsUser.All Sites.FullControl.All GroupMember.ReadWrite.All',
},
];
}
Loading

0 comments on commit 92cfadd

Please sign in to comment.