diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f859bed7a0..e5764af8c7 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -130,6 +130,14 @@ export type EndpointStyle = { hoverMessage?: string; }; +export type EndpointMeta = { + __meta?: { + index: number; + totalEndpoints: number; + endpointLabelLength: number; + }; +}; + export interface IUpdateInformation< T extends NodeParameterValueType = | string diff --git a/packages/editor-ui/src/__tests__/mocks.ts b/packages/editor-ui/src/__tests__/mocks.ts index 996a6a5a95..e934f9518f 100644 --- a/packages/editor-ui/src/__tests__/mocks.ts +++ b/packages/editor-ui/src/__tests__/mocks.ts @@ -14,6 +14,7 @@ import { uuid } from '@jsplumb/util'; import { defaultMockNodeTypes } from '@/__tests__/defaults'; import type { INodeUi, ITag, IUsedCredential, IWorkflowDb, WorkflowMetadata } from '@/Interface'; import type { ProjectSharingData } from '@/features/projects/projects.types'; +import type { RouteLocationNormalized } from 'vue-router'; export function createTestNodeTypes(data: INodeTypeData = {}): INodeTypes { const getResolvedKey = (key: string) => { @@ -103,3 +104,27 @@ export function createTestNode( ...node, }; } + +export function createTestRouteLocation({ + path = '', + params = {}, + fullPath = path, + hash = '', + matched = [], + redirectedFrom = undefined, + name = path, + meta = {}, + query = {}, +}: Partial = {}): RouteLocationNormalized { + return { + path, + params, + fullPath, + hash, + matched, + redirectedFrom, + name, + meta, + query, + }; +} diff --git a/packages/editor-ui/src/types/expressions.ts b/packages/editor-ui/src/types/expressions.ts index 24450ba46e..8175467db8 100644 --- a/packages/editor-ui/src/types/expressions.ts +++ b/packages/editor-ui/src/types/expressions.ts @@ -16,6 +16,7 @@ export type Resolvable = { resolved: unknown; state: ResolvableState; error: Error | null; + fullError?: Error; } & Range; export type Resolved = Resolvable; diff --git a/packages/editor-ui/src/utils/__tests__/rbacUtils.test.ts b/packages/editor-ui/src/utils/__tests__/rbacUtils.test.ts index a6612e8b50..c27a392293 100644 --- a/packages/editor-ui/src/utils/__tests__/rbacUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/rbacUtils.test.ts @@ -1,20 +1,20 @@ -import type { RouteLocationNormalized } from 'vue-router'; import { inferProjectIdFromRoute, inferResourceTypeFromRoute, inferResourceIdFromRoute, } from '../rbacUtils'; +import { createTestRouteLocation } from '@/__tests__/mocks'; describe('rbacUtils', () => { describe('inferProjectIdFromRoute()', () => { it('should infer project ID from route correctly', () => { - const route = { path: '/dashboard/projects/123/settings' } as RouteLocationNormalized; + const route = createTestRouteLocation({ path: '/dashboard/projects/123/settings' }); const projectId = inferProjectIdFromRoute(route); expect(projectId).toBe('123'); }); it('should return undefined for project ID if not found', () => { - const route = { path: '/dashboard/settings' } as RouteLocationNormalized; + const route = createTestRouteLocation({ path: '/dashboard/settings' }); const projectId = inferProjectIdFromRoute(route); expect(projectId).toBeUndefined(); }); @@ -29,15 +29,15 @@ describe('rbacUtils', () => { ['/variables', 'variable'], ['/users', 'user'], ['/source-control', 'sourceControl'], - ['/external-secrets', 'externalSecretsStore'], + ['/external-secrets', 'externalSecret'], ])('should infer resource type from %s correctly to %s', (path, type) => { - const route = { path } as RouteLocationNormalized; + const route = createTestRouteLocation({ path }); const resourceType = inferResourceTypeFromRoute(route); expect(resourceType).toBe(type); }); it('should return undefined for resource type if not found', () => { - const route = { path: '/dashboard/settings' } as RouteLocationNormalized; + const route = createTestRouteLocation({ path: '/dashboard/settings' }); const resourceType = inferResourceTypeFromRoute(route); expect(resourceType).toBeUndefined(); }); @@ -45,19 +45,19 @@ describe('rbacUtils', () => { describe('inferResourceIdFromRoute()', () => { it('should infer resource ID from params.id', () => { - const route = { params: { id: 'abc123' } } as RouteLocationNormalized; + const route = createTestRouteLocation({ params: { id: 'abc123' } }); const resourceId = inferResourceIdFromRoute(route); expect(resourceId).toBe('abc123'); }); it('should infer resource ID from params.name if id is not present', () => { - const route = { params: { name: 'my-resource' } } as RouteLocationNormalized; + const route = createTestRouteLocation({ params: { name: 'my-resource' } }); const resourceId = inferResourceIdFromRoute(route); expect(resourceId).toBe('my-resource'); }); it('should return undefined for resource ID if neither id nor name is present', () => { - const route = { params: {} } as RouteLocationNormalized; + const route = createTestRouteLocation({ params: {} }); const resourceId = inferResourceIdFromRoute(route); expect(resourceId).toBeUndefined(); }); diff --git a/packages/editor-ui/src/utils/__tests__/sourceControlUtils.test.ts b/packages/editor-ui/src/utils/__tests__/sourceControlUtils.test.ts deleted file mode 100644 index ef7141fcd9..0000000000 --- a/packages/editor-ui/src/utils/__tests__/sourceControlUtils.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import type { SourceControlStatus } from '@/Interface'; -import { beforeEach } from 'vitest'; -import { aggregateSourceControlFiles } from '@/utils/sourceControlUtils'; - -describe('sourceControlUtils', () => { - describe('aggregateSourceControlFiles()', () => { - let status: SourceControlStatus; - - beforeEach(() => { - status = { - ahead: 0, - behind: 0, - conflicted: [], - created: [], - current: 'main', - deleted: [], - detached: false, - files: [], - modified: [], - not_added: [], - renamed: [], - staged: [], - tracking: null, - }; - }); - - it('should be empty array if no files', () => { - expect(aggregateSourceControlFiles(status)).toEqual([]); - }); - - it('should contain list of conflicted, created, deleted, modified, and renamed files', () => { - status.files = [ - { path: 'conflicted.json', index: 'A', working_dir: '' }, - { path: 'created.json', index: 'A', working_dir: '' }, - { path: 'deleted.json', index: 'A', working_dir: '' }, - { path: 'modified.json', index: 'A', working_dir: '' }, - { path: 'renamed.json', index: 'A', working_dir: '' }, - ]; - - status.conflicted.push('conflicted.json'); - status.created.push('created.json'); - status.deleted.push('deleted.json'); - status.modified.push('modified.json'); - status.renamed.push('renamed.json'); - status.staged = status.files.map((file) => file.path); - - expect(aggregateSourceControlFiles(status)).toEqual([ - { path: 'conflicted.json', status: 'conflicted', staged: true }, - { path: 'created.json', status: 'created', staged: true }, - { path: 'deleted.json', status: 'deleted', staged: true }, - { path: 'modified.json', status: 'modified', staged: true }, - { path: 'renamed.json', status: 'renamed', staged: true }, - ]); - }); - }); -}); diff --git a/packages/editor-ui/src/utils/canvasUtils.ts b/packages/editor-ui/src/utils/canvasUtils.ts index 3d619a17d7..332d7c7569 100644 --- a/packages/editor-ui/src/utils/canvasUtils.ts +++ b/packages/editor-ui/src/utils/canvasUtils.ts @@ -3,7 +3,7 @@ import type { IZoomConfig } from '@/Interface'; import { useWorkflowsStore } from '@/stores/workflows.store'; import type { ConnectionDetachedParams } from '@jsplumb/core'; import type { IConnection } from 'n8n-workflow'; -import type { Route } from 'vue-router'; +import type { RouteLocation } from 'vue-router'; /* Constants and utility functions mainly used by canvas store @@ -52,7 +52,7 @@ export const scaleReset = (config: IZoomConfig): IZoomConfig => { return applyScale(1 / config.scale)(config); }; -export const getNodeViewTab = (route: Route): string | null => { +export const getNodeViewTab = (route: RouteLocation): string | null => { if (route.meta?.nodeView) { return MAIN_HEADER_TABS.WORKFLOW; } else if ( diff --git a/packages/editor-ui/src/utils/nodeTypesUtils.ts b/packages/editor-ui/src/utils/nodeTypesUtils.ts index 6263e4a33a..285817b209 100644 --- a/packages/editor-ui/src/utils/nodeTypesUtils.ts +++ b/packages/editor-ui/src/utils/nodeTypesUtils.ts @@ -143,8 +143,8 @@ export const getMainAuthField = (nodeType: INodeTypeDescription | null): INodePr credentialDependencies.find( (prop) => prop.name === MAIN_AUTH_FIELD_NAME && - !prop.options?.find((option) => option.value === 'none'), - ) || null; + !prop.options?.find((option) => 'value' in option && option.value === 'none'), + ) ?? null; // If there is a field name `authentication`, use it // Otherwise, try to find alternative main auth field const mainAuthFiled = @@ -166,7 +166,7 @@ const findAlternativeAuthField = ( if (cred.displayOptions?.show) { for (const fieldName in cred.displayOptions.show) { dependentAuthFieldValues[fieldName] = (dependentAuthFieldValues[fieldName] || []).concat( - (cred.displayOptions.show[fieldName] || []).map((val) => (val ? val.toString() : '')), + (cred.displayOptions.show[fieldName] ?? []).map((val) => (val ? val.toString() : '')), ); } } @@ -174,7 +174,11 @@ const findAlternativeAuthField = ( const alternativeAuthField = fields.find((field) => { let required = true; field.options?.forEach((option) => { - if (!dependentAuthFieldValues[field.name].includes(option.value)) { + if ( + 'value' in option && + typeof option.value === 'string' && + !dependentAuthFieldValues[field.name].includes(option.value) + ) { required = false; } }); @@ -206,21 +210,24 @@ export const getNodeAuthOptions = ( if (field.options) { options = options.concat( field.options.map((option) => { + const optionValue = 'value' in option ? `${option.value}` : ''; + // Check if credential type associated with this auth option has overwritten properties let hasOverrides = false; - const cred = getNodeCredentialForSelectedAuthType(nodeType, option.value); + const cred = getNodeCredentialForSelectedAuthType(nodeType, optionValue); if (cred) { hasOverrides = useCredentialsStore().getCredentialTypeByName(cred.name)?.__overwrittenProperties !== undefined; } + return { name: // Add recommended suffix if credentials have overrides and option is not already recommended hasOverrides && !option.name.endsWith(recommendedSuffix) ? `${option.name} ${recommendedSuffix}` : option.name, - value: option.value, + value: optionValue, // Also add in the display options so we can hide/show the option if necessary displayOptions: field.displayOptions, }; diff --git a/packages/editor-ui/src/utils/nodeViewUtils.ts b/packages/editor-ui/src/utils/nodeViewUtils.ts index bafa678912..d03f1a7874 100644 --- a/packages/editor-ui/src/utils/nodeViewUtils.ts +++ b/packages/editor-ui/src/utils/nodeViewUtils.ts @@ -1,16 +1,16 @@ -import { isNumber } from '@/utils/typeGuards'; +import { isNumber, isValidNodeConnectionType } from '@/utils/typeGuards'; import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE } from '@/constants'; -import type { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface'; +import type { EndpointMeta, EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface'; import type { ArrayAnchorSpec, ConnectorSpec, OverlaySpec, PaintStyle } from '@jsplumb/common'; -import type { Endpoint, Connection } from '@jsplumb/core'; +import type { Connection, Endpoint, SelectOptions } from '@jsplumb/core'; import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector'; import type { ConnectionTypes, IConnection, - ITaskData, INodeExecutionData, - NodeInputConnections, INodeTypeDescription, + ITaskData, + NodeInputConnections, } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; import type { BrowserJsPlumbInstance } from '@jsplumb/browser-ui'; @@ -72,13 +72,15 @@ export const CONNECTOR_FLOWCHART_TYPE: ConnectorSpec = { alwaysRespectStubs: false, loopbackVerticalLength: NODE_SIZE + GRID_SIZE, // height of vertical segment when looping loopbackMinimum: LOOPBACK_MINIMUM, // minimum length before flowchart loops around - getEndpointOffset(endpoint: Endpoint) { + getEndpointOffset(endpoint: Endpoint & EndpointMeta) { const indexOffset = 10; // stub offset between different endpoints of same node const index = endpoint?.__meta ? endpoint.__meta.index : 0; const totalEndpoints = endpoint?.__meta ? endpoint.__meta.totalEndpoints : 0; const outputOverlay = getOverlay(endpoint, OVERLAY_OUTPUT_NAME_LABEL); - const labelOffset = outputOverlay?.label && outputOverlay.label.length > 1 ? 10 : 0; + const outputOverlayLabel = + outputOverlay && 'label' in outputOverlay ? `${outputOverlay?.label}` : ''; + const labelOffset = outputOverlayLabel.length > 1 ? 10 : 0; const outputsOffset = totalEndpoints > 3 ? 24 : 0; // avoid intersecting plus return index * indexOffset + labelOffset + outputsOffset; @@ -111,6 +113,10 @@ export const CONNECTOR_PAINT_STYLE_DATA: PaintStyle = { stroke: 'var(--color-foreground-dark)', }; +export function isCanvasAugmentedType(overlay: T): overlay is T & { canvas: HTMLElement } { + return typeof overlay === 'object' && overlay !== null && 'canvas' in overlay && !!overlay.canvas; +} + export const getConnectorColor = (type: ConnectionTypes, category?: string): string => { if (category === 'error') { return '--color-node-error-output-text-color'; @@ -228,15 +234,18 @@ export const getAnchorPosition = ( return returnPositions; }; -export const getScope = (type?: string) => { +export const getScope = (type?: NodeConnectionType): NodeConnectionType | undefined => { if (!type || type === NodeConnectionType.Main) { return undefined; } + return type; }; -export const getEndpointScope = (endpointType: ConnectionTypes): string | undefined => { - if (Object.values(NodeConnectionType).includes(endpointType)) { +export const getEndpointScope = ( + endpointType: NodeConnectionType | string, +): NodeConnectionType | undefined => { + if (isValidNodeConnectionType(endpointType)) { return getScope(endpointType); } @@ -276,7 +285,7 @@ export const getInputNameOverlay = ( id: OVERLAY_INPUT_NAME_LABEL, visible: true, location: [-1, -1], - create: (component: Endpoint) => { + create: (_: Endpoint) => { const label = document.createElement('div'); label.innerHTML = labelText; if (required) { @@ -303,20 +312,20 @@ export const getOutputEndpointStyle = ( export const getOutputNameOverlay = ( labelText: string, - outputName: ConnectionTypes, + outputName: NodeConnectionType, category?: string, ): OverlaySpec => ({ type: 'Custom', options: { id: OVERLAY_OUTPUT_NAME_LABEL, visible: true, - create: (ep: Endpoint) => { + create: (ep: Endpoint & EndpointMeta) => { const label = document.createElement('div'); label.innerHTML = labelText; label.classList.add('node-output-endpoint-label'); if (ep?.__meta?.endpointLabelLength) { - label.setAttribute('data-endpoint-label-length', ep?.__meta?.endpointLabelLength); + label.setAttribute('data-endpoint-label-length', `${ep?.__meta?.endpointLabelLength}`); } label.classList.add(`node-connection-type-${getScope(outputName) ?? 'main'}`); if (outputName !== NodeConnectionType.Main) { @@ -438,7 +447,10 @@ export const showOrHideMidpointArrow = (connection: Connection) => { if (arrow) { arrow.setVisible(isArrowVisible); arrow.setLocation(hasItemsLabel ? 0.6 : 0.5); - connection.instance.repaint(arrow.canvas); + + if (isCanvasAugmentedType(arrow)) { + connection.instance.repaint(arrow.canvas); + } } }; @@ -481,7 +493,9 @@ export const showOrHideItemsLabel = (connection: Connection) => { const isHidden = diffX < MIN_X_TO_SHOW_OUTPUT_LABEL && diffY < MIN_Y_TO_SHOW_OUTPUT_LABEL; overlay.setVisible(!isHidden); - const innerElement = overlay.canvas?.querySelector('span'); + const innerElement = isCanvasAugmentedType(overlay) + ? overlay.canvas.querySelector('span') + : undefined; if (innerElement) { if (diffY === 0 || isLoopingBackwards(connection)) { innerElement.classList.add('floating'); @@ -812,8 +826,11 @@ export const addClassesToOverlays = ({ overlayIds.forEach((overlayId) => { const overlay = getOverlay(connection, overlayId); - overlay?.canvas?.classList.add(...classNames); - if (includeConnector) { + if (overlay && isCanvasAugmentedType(overlay)) { + overlay.canvas?.classList.add(...classNames); + } + + if (includeConnector && isCanvasAugmentedType(connection.connector)) { connection.connector.canvas?.classList.add(...classNames); } }); @@ -995,18 +1012,18 @@ export const addConnectionActionsOverlay = ( export const getOutputEndpointUUID = ( nodeId: string, - connectionType: ConnectionTypes, + connectionType: NodeConnectionType, outputIndex: number, ) => { - return `${nodeId}${OUTPUT_UUID_KEY}${getScope(connectionType) || ''}${outputIndex}`; + return `${nodeId}${OUTPUT_UUID_KEY}${getScope(connectionType) ?? ''}${outputIndex}`; }; export const getInputEndpointUUID = ( nodeId: string, - connectionType: ConnectionTypes, + connectionType: NodeConnectionType, inputIndex: number, ) => { - return `${nodeId}${INPUT_UUID_KEY}${getScope(connectionType) || ''}${inputIndex}`; + return `${nodeId}${INPUT_UUID_KEY}${getScope(connectionType) ?? ''}${inputIndex}`; }; export const getFixedNodesList = (workflowNodes: T[]): T[] => { @@ -1089,10 +1106,10 @@ export const getJSPlumbEndpoints = ( node: INodeUi | null, instance: BrowserJsPlumbInstance, ): Endpoint[] => { - const nodeEl = instance.getManagedElement(node?.id); + if (!node) return []; - const endpoints = instance?.getEndpoints(nodeEl); - return endpoints; + const nodeEl = instance.getManagedElement(node?.id); + return instance?.getEndpoints(nodeEl); }; export const getPlusEndpoint = ( @@ -1102,8 +1119,7 @@ export const getPlusEndpoint = ( ): Endpoint | undefined => { const endpoints = getJSPlumbEndpoints(node, instance); return endpoints.find( - (endpoint: Endpoint) => - // @ts-ignore + (endpoint: Endpoint & EndpointMeta) => endpoint.endpoint.type === 'N8nPlus' && endpoint?.__meta?.index === outputIndex, ); }; @@ -1113,7 +1129,7 @@ export const getJSPlumbConnection = ( sourceOutputIndex: number, targetNode: INodeUi | null, targetInputIndex: number, - connectionType: ConnectionTypes, + connectionType: NodeConnectionType, sourceNodeType: INodeTypeDescription | null, instance: BrowserJsPlumbInstance, ): Connection | undefined => { @@ -1127,17 +1143,24 @@ export const getJSPlumbConnection = ( const sourceEndpoint = getOutputEndpointUUID(sourceId, connectionType, sourceOutputIndex); const targetEndpoint = getInputEndpointUUID(targetId, connectionType, targetInputIndex); - const sourceNodeOutput = sourceNodeType?.outputs?.[sourceOutputIndex] || NodeConnectionType.Main; + const sourceNodeOutput = sourceNodeType?.outputs?.[sourceOutputIndex] ?? NodeConnectionType.Main; const sourceNodeOutputName = - typeof sourceNodeOutput === 'string' ? sourceNodeOutput : sourceNodeOutput.name; + typeof sourceNodeOutput === 'string' + ? sourceNodeOutput + : 'name' in sourceNodeOutput + ? `${sourceNodeOutput.name}` + : ''; const scope = getEndpointScope(sourceNodeOutputName); - // @ts-ignore const connections = instance?.getConnections({ scope, source: sourceId, target: targetId, - }) as Connection[]; + } as SelectOptions); + + if (!Array.isArray(connections)) { + return; + } return connections.find((connection: Connection) => { const uuids = connection.getUuids(); diff --git a/packages/editor-ui/src/utils/rbacUtils.ts b/packages/editor-ui/src/utils/rbacUtils.ts index 2608127992..3d76eaa43e 100644 --- a/packages/editor-ui/src/utils/rbacUtils.ts +++ b/packages/editor-ui/src/utils/rbacUtils.ts @@ -11,20 +11,24 @@ export function inferProjectIdFromRoute(to: RouteLocationNormalized): string { export function inferResourceTypeFromRoute(to: RouteLocationNormalized): Resource | undefined { const routeParts = to.path.split('/'); - const routeMap = { + const routeMap: Record = { workflow: 'workflows', credential: 'credentials', user: 'users', variable: 'variables', sourceControl: 'source-control', - externalSecretsStore: 'external-secrets', + externalSecret: 'external-secrets', }; - for (const resource of Object.keys(routeMap) as Array) { - if (routeParts.includes(routeMap[resource])) { + const isResource = (key: string): key is Resource => routeParts.includes(routeMap[key]); + + for (const resource of Object.keys(routeMap)) { + if (isResource(resource)) { return resource; } } + + return undefined; } export function inferResourceIdFromRoute(to: RouteLocationNormalized): string | undefined { diff --git a/packages/editor-ui/src/utils/sourceControlUtils.ts b/packages/editor-ui/src/utils/sourceControlUtils.ts deleted file mode 100644 index fc336eddb0..0000000000 --- a/packages/editor-ui/src/utils/sourceControlUtils.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { SourceControlAggregatedFile, SourceControlStatus } from '@/Interface'; - -export function aggregateSourceControlFiles(sourceControlStatus: SourceControlStatus) { - return sourceControlStatus.files.reduce((acc, file) => { - const staged = sourceControlStatus.staged.includes(file.path); - - let status = ''; - ( - ['conflicted', 'created', 'deleted', 'modified', 'renamed'] as Array< - keyof SourceControlStatus - > - ).forEach((key) => { - const filesForStatus = sourceControlStatus[key] as string[]; - if (filesForStatus.includes(file.path)) { - status = key; - } - }); - - acc.push({ - path: file.path, - status, - staged, - }); - - return acc; - }, []); -} diff --git a/packages/editor-ui/src/utils/telemetryUtils.ts b/packages/editor-ui/src/utils/telemetryUtils.ts index b7d1e5cadb..dd8c750398 100644 --- a/packages/editor-ui/src/utils/telemetryUtils.ts +++ b/packages/editor-ui/src/utils/telemetryUtils.ts @@ -26,12 +26,14 @@ export function createExpressionTelemetryPayload( handlebar_error_count: erroringResolvables.length, short_errors: erroringResolvables.map((r) => r.resolved ?? null), full_errors: erroringResolvables.map((erroringResolvable) => { - if (!erroringResolvable.fullError) return null; + if (erroringResolvable.fullError) { + return { + ...exposeErrorProperties(erroringResolvable.fullError), + stack: erroringResolvable.fullError.stack, + }; + } - return { - ...exposeErrorProperties(erroringResolvable.fullError), - stack: erroringResolvable.fullError.stack, - }; + return null; }), }; } diff --git a/packages/editor-ui/src/utils/testData/templateTestData.ts b/packages/editor-ui/src/utils/testData/templateTestData.ts index 6a1e156063..c878acf72f 100644 --- a/packages/editor-ui/src/utils/testData/templateTestData.ts +++ b/packages/editor-ui/src/utils/testData/templateTestData.ts @@ -295,17 +295,14 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = { export const fullCreateApiEndpointTemplate = { id: 1750, name: 'Creating an API endpoint', - views: 13265, recentViews: 9899, totalViews: 13265, createdAt: '2022-07-06T14:45:19.659Z', description: '**Task:**\nCreate a simple API endpoint using the Webhook and Respond to Webhook nodes\n\n**Why:**\nYou can prototype or replace a backend process with a single workflow\n\n**Main use cases:**\nReplace backend logic with a workflow', workflow: { - meta: { instanceId: '8c8c5237b8e37b006a7adce87f4369350c58e41f3ca9de16196d3197f69eabcd' }, nodes: [ { - id: 'f80aceed-b676-42aa-bf25-f7a44408b1bc', name: 'Webhook', type: 'n8n-nodes-base.webhook', position: [375, 115], @@ -318,7 +315,6 @@ export const fullCreateApiEndpointTemplate = { typeVersion: 1, }, { - id: '3b9ec913-0bbe-4906-bf8e-da352b556655', name: 'Note1', type: 'n8n-nodes-base.stickyNote', position: [355, -25], @@ -331,7 +327,6 @@ export const fullCreateApiEndpointTemplate = { typeVersion: 1, }, { - id: '9c36dae5-0700-450c-9739-e9f3eff31bfe', name: 'Respond to Webhook', type: 'n8n-nodes-base.respondToWebhook', position: [815, 115], @@ -344,7 +339,6 @@ export const fullCreateApiEndpointTemplate = { typeVersion: 1, }, { - id: '5a228fcb-78b9-4a28-95d2-d7c9fdf1d4ea', name: 'Create URL string', type: 'n8n-nodes-base.set', position: [595, 115], @@ -364,7 +358,6 @@ export const fullCreateApiEndpointTemplate = { typeVersion: 1, }, { - id: 'e7971820-45a8-4dc8-ba4c-b3220d65307a', name: 'Note3', type: 'n8n-nodes-base.stickyNote', position: [355, 275], @@ -383,7 +376,10 @@ export const fullCreateApiEndpointTemplate = { }, }, lastUpdatedBy: 1, - workflowInfo: null, + workflowInfo: { + nodeCount: 2, + nodeTypes: {}, + }, user: { username: 'jon-n8n' }, nodes: [ { diff --git a/packages/editor-ui/src/utils/typeGuards.ts b/packages/editor-ui/src/utils/typeGuards.ts index 00e10a560e..ba32e1ae2d 100644 --- a/packages/editor-ui/src/utils/typeGuards.ts +++ b/packages/editor-ui/src/utils/typeGuards.ts @@ -1,4 +1,5 @@ -import type { INodeParameterResourceLocator } from 'n8n-workflow'; +import type { INodeParameterResourceLocator, NodeConnectionType } from 'n8n-workflow'; +import { nodeConnectionTypes } from 'n8n-workflow'; import type { ICredentialsResponse, NewCredentialsModal } from '@/Interface'; /* @@ -49,3 +50,9 @@ export function isDateObject(date: unknown): date is Date { !!date && Object.prototype.toString.call(date) === '[object Date]' && !isNaN(date as number) ); } + +export function isValidNodeConnectionType( + connectionType: string, +): connectionType is NodeConnectionType { + return nodeConnectionTypes.includes(connectionType as NodeConnectionType); +} diff --git a/packages/editor-ui/src/utils/typesUtils.ts b/packages/editor-ui/src/utils/typesUtils.ts index 4fdf68b4c6..776bc2fc63 100644 --- a/packages/editor-ui/src/utils/typesUtils.ts +++ b/packages/editor-ui/src/utils/typesUtils.ts @@ -77,20 +77,17 @@ export function shorten(s: string, limit: number, keep: number) { export const convertPath = (path: string): string => { // TODO: That can for sure be done fancier but for now it works const placeholder = '*___~#^#~___*'; - let inBrackets = path.match(/\[(.*?)]/g); + let inBrackets: string[] = path.match(/\[(.*?)]/g) ?? []; + + inBrackets = inBrackets + .map((item) => item.slice(1, -1)) + .map((item) => { + if (item.startsWith('"') && item.endsWith('"')) { + return item.slice(1, -1); + } + return item; + }); - if (inBrackets === null) { - inBrackets = []; - } else { - inBrackets = inBrackets - .map((item) => item.slice(1, -1)) - .map((item) => { - if (item.startsWith('"') && item.endsWith('"')) { - return item.slice(1, -1); - } - return item; - }); - } const withoutBrackets = path.replace(/\[(.*?)]/g, placeholder); const pathParts = withoutBrackets.split('.'); const allParts = [] as string[]; @@ -98,7 +95,7 @@ export const convertPath = (path: string): string => { let index = part.indexOf(placeholder); while (index !== -1) { if (index === 0) { - allParts.push(inBrackets!.shift() as string); + allParts.push(inBrackets.shift() ?? ''); part = part.substr(placeholder.length); } else { allParts.push(part.substr(0, index)); diff --git a/packages/editor-ui/src/utils/userUtils.ts b/packages/editor-ui/src/utils/userUtils.ts index 5646c004e7..a3925b7534 100644 --- a/packages/editor-ui/src/utils/userUtils.ts +++ b/packages/editor-ui/src/utils/userUtils.ts @@ -135,7 +135,7 @@ function getPersonalizationSurveyV2OrLater( const companySize = answers[COMPANY_SIZE_KEY]; const companyType = answers[COMPANY_TYPE_KEY]; - const automationGoal = answers[AUTOMATION_GOAL_KEY]; + const automationGoal = AUTOMATION_GOAL_KEY in answers ? answers[AUTOMATION_GOAL_KEY] : undefined; let codingSkill = null; if (CODING_SKILL_KEY in answers && answers[CODING_SKILL_KEY]) { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 7fc62bb6bf..fe8e6baf1b 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1704,6 +1704,21 @@ export const enum NodeConnectionType { Main = 'main', } +export const nodeConnectionTypes: NodeConnectionType[] = [ + NodeConnectionType.AiAgent, + NodeConnectionType.AiChain, + NodeConnectionType.AiDocument, + NodeConnectionType.AiEmbedding, + NodeConnectionType.AiLanguageModel, + NodeConnectionType.AiMemory, + NodeConnectionType.AiOutputParser, + NodeConnectionType.AiRetriever, + NodeConnectionType.AiTextSplitter, + NodeConnectionType.AiTool, + NodeConnectionType.AiVectorStore, + NodeConnectionType.Main, +]; + export interface INodeInputFilter { // TODO: Later add more filter options like categories, subcatogries, // regex, allow to exclude certain nodes, ... ?