refactor(editor): Fix remaining FE type check errors (no-changelog) (#9607)

Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
Ricardo Espinoza 2024-06-10 09:23:06 -04:00 committed by GitHub
parent 1e15f73b0d
commit 22bdb0568e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 438 additions and 318 deletions

View file

@ -240,7 +240,7 @@ const validationError = computed<string | null>(() => {
if (error) {
if ('messageKey' in error) {
return t(error.messageKey, error.options as object);
return t(error.messageKey, error.options);
} else if ('message' in error) {
return error.message;
}

View file

@ -6,6 +6,9 @@ const RE_NARGS = /(%|)\{([0-9a-zA-Z_]+)\}/g;
* https://github.com/Matt-Esch/string-template/index.js
*/
export default function () {
const isReplacementGroup = (target: object, key: string): target is Record<string, unknown> =>
key in target;
function template(
value: string | ((...args: unknown[]) => string),
...args: Array<string | object>
@ -15,21 +18,23 @@ export default function () {
}
const str = value;
let replacements: object = args;
if (args.length === 1 && typeof args[0] === 'object') {
args = args[0] as unknown as Array<string | object>;
replacements = args[0];
}
if (!args?.hasOwnProperty) {
args = {} as unknown as Array<string | object>;
if (!replacements?.hasOwnProperty) {
replacements = {};
}
return str.replace(RE_NARGS, (match, _, i, index: number) => {
let result: string | object | null;
return str.replace(RE_NARGS, (match, _, group: string, index: number): string => {
let result: string | null;
if (str[index - 1] === '{' && str[index + match.length] === '}') {
return i;
return `${group}`;
} else {
result = Object.hasOwn(args, i) ? args[i] : null;
result = isReplacementGroup(replacements, group) ? `${replacements[group]}` : null;
if (result === null || result === undefined) {
return '';
}

View file

@ -26,7 +26,7 @@ export const t = function (
// only support flat keys
if (lang[path] !== undefined) {
return format(lang[path], options);
return format(lang[path], ...(options ? [options] : []));
}
return '';
@ -44,8 +44,8 @@ export async function use(l: string) {
} catch (e) {}
}
export const i18n = function (fn: N8nLocaleTranslateFn) {
export function i18n(fn: N8nLocaleTranslateFn) {
i18nHandler = fn || i18nHandler;
};
}
export default { use, t, i18n };

View file

@ -2,7 +2,7 @@ import { t } from '../locale';
export default {
methods: {
t(path: string, options: object) {
t(path: string, options: string[]) {
return t.call(this, path, options);
},
},

View file

@ -1,7 +0,0 @@
import * as Vue from 'vue';
declare module 'vue/types/vue' {
interface Vue {
$style: Record<string, string>;
}
}

View file

@ -1,4 +1,11 @@
declare module '*.vue' {
import Vue from 'vue';
export default Vue;
export {};
/**
* @docs https://vuejs.org/guide/typescript/options-api.html#augmenting-global-properties
*/
declare module 'vue' {
interface ComponentCustomProperties {
$style: Record<string, string>;
}
}

View file

@ -1,8 +1,10 @@
import type { N8nLocaleTranslateFnOptions } from 'n8n-design-system/types/i18n';
export type Rule = { name: string; config?: unknown };
export type RuleGroup = {
rules: Array<Rule | RuleGroup>;
defaultError?: { messageKey: string; options?: unknown };
defaultError?: { messageKey: string; options?: N8nLocaleTranslateFnOptions };
};
export type Validatable = string | number | boolean | null | undefined;
@ -13,8 +15,8 @@ export type IValidator<T = unknown> = {
config: T,
) =>
| false
| { message: string; options?: unknown }
| { messageKey: string; options?: unknown }
| { message: string; options?: N8nLocaleTranslateFnOptions }
| { messageKey: string; options?: N8nLocaleTranslateFnOptions }
| null;
};

View file

@ -1,3 +1,5 @@
export type N8nLocaleTranslateFn = (path: string, options: object) => string;
export type N8nLocaleTranslateFnOptions = string[] | Record<string, unknown>;
export type N8nLocaleTranslateFn = (path: string, options?: N8nLocaleTranslateFnOptions) => string;
export type N8nLocale = Record<string, string | ((...args: unknown[]) => string)>;

View file

@ -134,16 +134,6 @@ export type EndpointStyle = {
hoverMessage?: string;
};
export type EndpointMeta = {
__meta?: {
nodeName: string;
nodeId: string;
index: number;
totalEndpoints: number;
endpointLabelLength: number;
};
};
export interface IUpdateInformation<T extends NodeParameterValueType = NodeParameterValueType> {
name: string;
key?: string;
@ -410,19 +400,6 @@ export interface IExecutionResponse extends IExecutionBase {
executedNode?: string;
}
export interface IExecutionShortResponse {
id: string;
workflowData: {
id: string;
name: string;
};
mode: WorkflowExecuteMode;
finished: boolean;
startedAt: Date;
stoppedAt: Date;
executionTime?: number;
}
export interface IExecutionsListResponse {
count: number;
results: ExecutionSummary[];
@ -432,6 +409,7 @@ export interface IExecutionsListResponse {
export interface IExecutionsCurrentSummaryExtended {
id: string;
finished?: boolean;
status: ExecutionStatus;
mode: WorkflowExecuteMode;
retryOf?: string | null;
retrySuccessId?: string | null;

View file

@ -1,5 +1,10 @@
import { faker } from '@faker-js/faker';
import type { ProjectListItem, ProjectSharingData, ProjectType } from '@/types/projects.types';
import type {
Project,
ProjectListItem,
ProjectSharingData,
ProjectType,
} from '@/types/projects.types';
import { ProjectTypes } from '@/types/projects.types';
export const createProjectSharingData = (projectType?: ProjectType): ProjectSharingData => ({
@ -19,3 +24,16 @@ export const createProjectListItem = (projectType?: ProjectType): ProjectListIte
updatedAt: faker.date.recent().toISOString(),
};
};
export function createTestProject(data: Partial<Project>): Project {
return {
id: faker.string.uuid(),
name: faker.lorem.words({ min: 1, max: 3 }),
createdAt: faker.date.past().toISOString(),
updatedAt: faker.date.recent().toISOString(),
type: 'team',
relations: [],
scopes: [],
...data,
};
}

View file

@ -4,7 +4,7 @@ import type { EnvironmentVariable } from '@/Interface';
export const variableFactory = Factory.extend<EnvironmentVariable>({
id(i: number) {
return i;
return `${i}`;
},
key() {
return `${faker.lorem.word()}`.toUpperCase();

View file

@ -31,7 +31,7 @@ export async function deleteDestinationFromDb(context: IRestApiContext, destinat
export async function sendTestMessageToDestination(
context: IRestApiContext,
destination: ApiMessageEventBusDestinationOptions,
) {
): Promise<boolean> {
const data: IDataObject = {
...destination,
};

View file

@ -5,9 +5,9 @@ import type {
INodePropertyOptions,
INodeTypeDescription,
INodeTypeNameVersion,
ResourceMapperFields,
} from 'n8n-workflow';
import axios from 'axios';
import type { ResourceMapperFields } from 'n8n-workflow/src/Interfaces';
export async function getNodeTypes(baseUrl: string) {
const { data } = await axios.get(baseUrl + 'types/nodes.json', { withCredentials: true });

View file

@ -11,7 +11,7 @@ import type {
} from '@/Interface';
import { get } from '@/utils/apiUtils';
function stringifyArray(arr: number[]) {
function stringifyArray(arr: string[]) {
return arr.join(',');
}
@ -41,7 +41,7 @@ export async function getCollections(
export async function getWorkflows(
apiEndpoint: string,
query: { page: number; limit: number; categories: number[]; search: string },
query: { page: number; limit: number; categories: string[]; search: string },
headers?: RawAxiosRequestHeaders,
): Promise<{
totalWorkflows: number;

View file

@ -1115,8 +1115,6 @@ export default defineComponent({
oauthTokenData: {} as CredentialInformation,
};
this.credentialsStore.enableOAuthCredential(credential);
// Close the window
if (oauthPopup) {
oauthPopup.close();
@ -1164,7 +1162,7 @@ export default defineComponent({
this.credentialData = {
...this.credentialData,
scopes,
scopes: scopes as unknown as CredentialInformation,
homeProject,
};
},

View file

@ -72,7 +72,7 @@
</template>
<script lang="ts">
import type { ICredentialsResponse, IUser, IUserListAction } from '@/Interface';
import type { ICredentialsResponse, IUserListAction } from '@/Interface';
import { defineComponent } from 'vue';
import type { PropType } from 'vue';
import { useMessage } from '@/composables/useMessage';
@ -157,22 +157,35 @@ export default defineComponent({
credentialOwnerName(): string {
return this.credentialsStore.getCredentialOwnerNameById(`${this.credentialId}`);
},
credentialDataHomeProject(): ProjectSharingData | undefined {
const credentialContainsProjectSharingData = (
data: ICredentialDataDecryptedObject,
): data is { homeProject: ProjectSharingData } => {
return 'homeProject' in data;
};
return this.credentialData && credentialContainsProjectSharingData(this.credentialData)
? this.credentialData.homeProject
: undefined;
},
isCredentialSharedWithCurrentUser(): boolean {
return (this.credentialData.sharedWithProjects ?? []).some((sharee: IUser) => {
return sharee.id === this.usersStore.currentUser?.id;
if (!Array.isArray(this.credentialData.sharedWithProjects)) return false;
return this.credentialData.sharedWithProjects.some((sharee) => {
return typeof sharee === 'object' && 'id' in sharee
? sharee.id === this.usersStore.currentUser?.id
: false;
});
},
projects(): ProjectListItem[] {
return this.projectsStore.personalProjects.filter(
(project) =>
project.id !== this.credential?.homeProject?.id &&
project.id !== this.credentialData?.homeProject?.id,
project.id !== this.credentialDataHomeProject?.id,
);
},
homeProject(): ProjectSharingData | undefined {
return (
this.credential?.homeProject ?? (this.credentialData?.homeProject as ProjectSharingData)
);
return this.credential?.homeProject ?? this.credentialDataHomeProject;
},
isHomeTeamProject(): boolean {
return this.homeProject?.type === ProjectTypes.Team;

View file

@ -2,7 +2,7 @@
<div
:class="$style.wrapper"
:style="iconStyleData"
@click="(e) => $emit('click')"
@click="() => $emit('click')"
@mouseover="showTooltip = true"
@mouseleave="showTooltip = false"
>
@ -126,7 +126,7 @@ export default defineComponent({
const restUrl = this.rootStore.getRestUrl;
if (nodeType.icon) {
if (typeof nodeType.icon === 'string') {
const [type, path] = nodeType.icon.split(':');
const returnData: NodeIconData = {
type,

View file

@ -207,6 +207,10 @@ const isWorkflowHistoryButtonDisabled = computed(() => {
return isNewWorkflow.value;
});
const workflowTagIds = computed(() => {
return (props.workflow.tags ?? []).map((tag) => (typeof tag === 'string' ? tag : tag.id));
});
watch(
() => props.workflow.id,
() => {
@ -601,7 +605,7 @@ function showCreateWorkflowSuccessToast(id?: string) {
<TagsContainer
v-else
:key="workflow.id"
:tag-ids="workflow.tags"
:tag-ids="workflowTagIds"
:clickable="true"
:responsive="true"
data-test-id="workflow-tags"

View file

@ -88,7 +88,9 @@ function onSelected(item: INodeCreateElement) {
const icon = item.properties.iconUrl
? `${baseUrl}${item.properties.iconUrl}`
: item.properties.icon?.split(':')[1];
: typeof item.properties.icon === 'string'
? item.properties.icon?.split(':')[1]
: undefined;
const transformedActions = nodeActions?.map((a) =>
transformNodeType(a, item.properties.displayName, 'action'),

View file

@ -18,7 +18,7 @@ export const mockSimplifiedNodeType = (
): SimplifiedNodeType => ({
displayName: 'Sample DisplayName',
name: 'sampleName',
icon: 'sampleIcon',
icon: 'fa:sampleIcon',
iconUrl: 'https://example.com/icon.png',
group: ['group1', 'group2'],
description: 'Sample description',

View file

@ -36,7 +36,7 @@ import { useI18n } from '@/composables/useI18n';
import { useKeyboardNavigation } from './useKeyboardNavigation';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { INodeInputFilter, NodeConnectionType } from 'n8n-workflow';
import type { INodeInputFilter, NodeConnectionType, Themed } from 'n8n-workflow';
import { useCanvasStore } from '@/stores/canvas.store';
interface ViewStack {
@ -48,7 +48,7 @@ interface ViewStack {
info?: string;
nodeIcon?: {
iconType?: string;
icon?: string;
icon?: Themed<string>;
color?: string;
};
iconUrl?: string;

View file

@ -58,7 +58,7 @@ import {
import { useI18n } from '@/composables/useI18n';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { SimplifiedNodeType } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow';
import type { INodeTypeDescription, Themed } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { useTemplatesStore } from '@/stores/templates.store';
@ -74,7 +74,7 @@ export interface NodeViewItem {
properties: {
name?: string;
title?: string;
icon?: string;
icon?: Themed<string>;
iconProps?: {
color?: string;
};

View file

@ -16,7 +16,7 @@
</template>
<script setup lang="ts">
import type { IVersionNode } from '@/Interface';
import type { IVersionNode, SimplifiedNodeType } from '@/Interface';
import { useRootStore } from '@/stores/n8nRoot.store';
import { useUIStore } from '@/stores/ui.store';
import { getBadgeIconUrl, getNodeIcon, getNodeIconUrl } from '@/utils/nodeTypesUtils';
@ -30,7 +30,7 @@ interface NodeIconSource {
}
type Props = {
nodeType?: INodeTypeDescription | IVersionNode | null;
nodeType?: INodeTypeDescription | SimplifiedNodeType | IVersionNode | null;
size?: number;
disabled?: boolean;
circle?: boolean;

View file

@ -460,7 +460,6 @@ export default defineComponent({
parameters: {},
} as INodeParameters,
nodeValuesInitialized: false, // Used to prevent nodeValues from being overwritten by defaults on reopening ndv
nodeSettings: [] as INodeProperties[],
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
CUSTOM_NODES_DOCS_URL,
@ -469,7 +468,7 @@ export default defineComponent({
};
},
watch: {
node(newNode, oldNode) {
node() {
this.setNodeValues();
},
},

View file

@ -28,7 +28,7 @@ import { isCommunityPackageName } from '@/utils/nodeTypesUtils';
type Tab = 'settings' | 'params';
type Props = {
modelValue?: Tab;
nodeType?: INodeTypeDescription;
nodeType?: INodeTypeDescription | null;
pushRef?: string;
};

View file

@ -1,6 +1,7 @@
import { createComponentRenderer } from '@/__tests__/render';
import ProjectTabs from '@/components/Projects/ProjectTabs.vue';
import { useRoute, useRouter } from 'vue-router';
import { createTestProject } from '@/__tests__/data/projects';
import { useProjectsStore } from '@/stores/projects.store';
vi.mock('vue-router', () => {
@ -54,15 +55,14 @@ describe('ProjectTabs', () => {
it('should render project tabs if use has permissions', () => {
route.params.projectId = '123';
projectsStore.currentProject = {
id: '123',
type: 'team',
name: 'Project',
relations: [],
scopes: ['project:update'],
createdAt: '',
updatedAt: '',
};
vi.mocked(useProjectsStore).mockImplementationOnce(
() =>
({
currentProject: createTestProject({
scopes: ['project:update'],
}),
}) as ReturnType<typeof useProjectsStore>,
);
const { getByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument();
@ -72,15 +72,14 @@ describe('ProjectTabs', () => {
it('should render project tabs', () => {
route.params.projectId = '123';
projectsStore.currentProject = {
id: '123',
type: 'team',
name: 'Project',
relations: [],
scopes: ['project:read'],
createdAt: '',
updatedAt: '',
};
vi.mocked(useProjectsStore).mockImplementationOnce(
() =>
({
currentProject: createTestProject({
scopes: ['project:read'],
}),
}) as ReturnType<typeof useProjectsStore>,
);
const { queryByText, getByText } = renderComponent();
expect(getByText('Workflows')).toBeInTheDocument();

View file

@ -486,7 +486,7 @@ export default defineComponent({
this.eventBus.on('refreshList', this.refreshList);
window.addEventListener('resize', this.setWidth);
useNDVStore().$subscribe((mutation, state) => {
useNDVStore().$subscribe((_mutation, _state) => {
// Update the width when main panel dimension change
this.setWidth();
});

View file

@ -16,7 +16,7 @@ import MappingModeSelect from './MappingModeSelect.vue';
import MatchingColumnsSelect from './MatchingColumnsSelect.vue';
import MappingFields from './MappingFields.vue';
import { fieldCannotBeDeleted, parseResourceMapperFieldName } from '@/utils/nodeTypesUtils';
import { isResourceMapperValue } from '@/utils/typeGuards';
import { isFullExecutionResponse, isResourceMapperValue } from '@/utils/typeGuards';
import { i18n as locale } from '@/plugins/i18n';
import { useNDVStore } from '@/stores/ndv.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
@ -78,7 +78,12 @@ watch(
watch(
() => workflowsStore.getWorkflowExecution,
async (data) => {
if (data?.status === 'success' && state.paramValue.mappingMode === 'autoMapInputData') {
if (
data &&
isFullExecutionResponse(data) &&
data.status === 'success' &&
state.paramValue.mappingMode === 'autoMapInputData'
) {
await initFetching(true);
}
},

View file

@ -176,12 +176,14 @@ import ParameterInputList from '@/components/ParameterInputList.vue';
import type { IMenuItem, INodeUi, IUpdateInformation } from '@/Interface';
import type {
IDataObject,
INodeCredentials,
NodeParameterValue,
MessageEventBusDestinationOptions,
INodeParameters,
NodeParameterValueType,
} from 'n8n-workflow';
import {
deepCopy,
messageEventBusDestinationTypeNames,
defaultMessageEventBusDestinationOptions,
defaultMessageEventBusDestinationWebhookOptions,
MessageEventBusDestinationTypeNames,
@ -246,7 +248,7 @@ export default defineComponent({
showRemoveConfirm: false,
typeSelectValue: '',
typeSelectPlaceholder: 'Destination Type',
nodeParameters: deepCopy(defaultMessageEventBusDestinationOptions),
nodeParameters: deepCopy(defaultMessageEventBusDestinationOptions) as INodeParameters,
webhookDescription: webhookModalDescription,
sentryDescription: sentryModalDescription,
syslogDescription: syslogModalDescription,
@ -261,7 +263,7 @@ export default defineComponent({
...mapStores(useUIStore, useLogStreamingStore, useNDVStore, useWorkflowsStore),
typeSelectOptions(): Array<{ value: string; label: BaseTextKey }> {
const options: Array<{ value: string; label: BaseTextKey }> = [];
for (const t of Object.values(MessageEventBusDestinationTypeNames)) {
for (const t of messageEventBusDestinationTypeNames) {
if (t === MessageEventBusDestinationTypeNames.abstract) {
continue;
}
@ -325,7 +327,8 @@ export default defineComponent({
if (arg.name === this.destination.id) {
if ('credentials' in arg.properties) {
this.unchanged = false;
this.nodeParameters.credentials = arg.properties.credentials as INodeCredentials;
this.nodeParameters.credentials = arg.properties
.credentials as NodeParameterValueType;
}
}
}
@ -350,7 +353,7 @@ export default defineComponent({
this.workflowsStore.removeNode(this.node);
this.ndvStore.activeNodeName = options.id ?? 'thisshouldnothappen';
this.workflowsStore.addNode(destinationToFakeINodeUi(options));
this.nodeParameters = options;
this.nodeParameters = options as INodeParameters;
this.logStreamingStore.items[this.destination.id].destination = options;
},
onTypeSelectInput(destinationType: MessageEventBusDestinationTypeNames) {
@ -448,7 +451,7 @@ export default defineComponent({
if (deleteConfirmed !== MODAL_CONFIRM) {
return;
} else {
this.eventBus.emit('remove', this.destination.id);
this.callEventBus('remove', this.destination.id);
this.uiStore.closeModal(LOG_STREAM_MODAL_KEY);
this.uiStore.stateIsDirty = false;
}
@ -456,10 +459,12 @@ export default defineComponent({
onModalClose() {
if (!this.hasOnceBeenSaved) {
this.workflowsStore.removeNode(this.node);
this.logStreamingStore.removeDestination(this.nodeParameters.id!);
if (this.nodeParameters.id) {
this.logStreamingStore.removeDestination(this.nodeParameters.id.toString());
}
}
this.ndvStore.activeNodeName = null;
this.eventBus.emit('closing', this.destination.id);
this.callEventBus('closing', this.destination.id);
this.uiStore.stateIsDirty = false;
},
async saveDestination() {
@ -471,10 +476,12 @@ export default defineComponent({
this.hasOnceBeenSaved = true;
this.testMessageSent = false;
this.unchanged = true;
this.eventBus.emit('destinationWasSaved', this.destination.id);
this.callEventBus('destinationWasSaved', this.destination.id);
this.uiStore.stateIsDirty = false;
const destinationType = (this.nodeParameters.__type ?? 'unknown')
const destinationType = (
this.nodeParameters.__type ? `${this.nodeParameters.__type}` : 'unknown'
)
.replace('$$MessageEventBusDestination', '')
.toLowerCase();
@ -503,6 +510,11 @@ export default defineComponent({
});
}
},
callEventBus(event: string, data: unknown) {
if (this.eventBus) {
this.eventBus.emit(event, data);
}
},
},
});
</script>

View file

@ -184,7 +184,7 @@ export default defineComponent({
},
isSelected(): boolean {
return (
this.uiStore.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name) !==
this.uiStore.getSelectedNodes.find((node: INodeUi) => node.name === this.data?.name) !==
undefined
);
},

View file

@ -12,6 +12,8 @@ import { useUsersStore } from '@/stores/users.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { testingNodeTypes, mockNodeTypesToArray } from '@/__tests__/defaults';
import { setupServer } from '@/__tests__/server';
import { NodeConnectionType } from 'n8n-workflow';
import type { IConnections } from 'n8n-workflow';
const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
props: {
@ -23,25 +25,25 @@ const renderComponent = createComponentRenderer(WorkflowLMChatModal, {
async function createPiniaWithAINodes(options = { withConnections: true, withAgentNode: true }) {
const { withConnections, withAgentNode } = options;
const workflowId = uuid();
const connections: IConnections = {
'Chat Trigger': {
main: [
[
{
node: 'Agent',
type: NodeConnectionType.Main,
index: 0,
},
],
],
},
};
const workflow = createTestWorkflow({
id: workflowId,
name: 'Test Workflow',
connections: withConnections
? {
'Chat Trigger': {
main: [
[
{
node: 'Agent',
type: 'main',
index: 0,
},
],
],
},
}
: {},
active: true,
...(withConnections ? { connections } : {}),
nodes: [
createTestNode({
name: 'Chat Trigger',

View file

@ -160,14 +160,14 @@ import { useRoute } from 'vue-router';
// eslint-disable-next-line unused-imports/no-unused-imports, @typescript-eslint/no-unused-vars
import type { BaseTextKey } from '@/plugins/i18n';
export interface IResource {
export type IResource = {
id: string;
name: string;
value: string;
updatedAt?: string;
createdAt?: string;
homeProject?: ProjectSharingData;
}
};
interface IFilters {
search: string;
@ -291,11 +291,11 @@ export default defineComponent({
case 'lastUpdated':
return props.sortFns.lastUpdated
? props.sortFns.lastUpdated(a, b)
: new Date(b.updatedAt).valueOf() - new Date(a.updatedAt).valueOf();
: new Date(b.updatedAt ?? '').valueOf() - new Date(a.updatedAt ?? '').valueOf();
case 'lastCreated':
return props.sortFns.lastCreated
? props.sortFns.lastCreated(a, b)
: new Date(b.createdAt).valueOf() - new Date(a.createdAt).valueOf();
: new Date(b.createdAt ?? '').valueOf() - new Date(a.createdAt ?? '').valueOf();
case 'nameAsc':
return props.sortFns.nameAsc
? props.sortFns.nameAsc(a, b)

View file

@ -69,7 +69,7 @@ describe('useCanvasPanning()', () => {
const { onMouseDown, onMouseMove, onMouseUp } = useCanvasPanning(elementRef);
onMouseDown(new MouseEvent('mousedown', { button: 1 }), true);
onMouseUp(new MouseEvent('mouseup'));
onMouseUp();
expect(removeEventListenerSpy).toHaveBeenCalledWith('mousemove', onMouseMove);
});

View file

@ -9,6 +9,7 @@ import { createPinia, setActivePinia } from 'pinia';
import { createTestNode } from '@/__tests__/mocks';
import type { Connection } from '@vue-flow/core';
import type { IConnection } from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
describe('useCanvasOperations', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
@ -200,9 +201,9 @@ describe('useCanvasOperations', () => {
const connection: Connection = {
source: nodeA.id,
sourceHandle: 'outputs/main/0',
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
target: nodeB.id,
targetHandle: 'inputs/main/0',
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
};
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
@ -211,8 +212,8 @@ describe('useCanvasOperations', () => {
expect(addConnectionSpy).toHaveBeenCalledWith({
connection: [
{ index: 0, node: nodeA.name, type: 'main' },
{ index: 0, node: nodeB.name, type: 'main' },
{ index: 0, node: nodeA.name, type: NodeConnectionType.Main },
{ index: 0, node: nodeB.name, type: NodeConnectionType.Main },
],
});
expect(uiStore.stateIsDirty).toBe(true);
@ -269,9 +270,9 @@ describe('useCanvasOperations', () => {
const connection: Connection = {
source: nodeA.id,
sourceHandle: 'outputs/main/0',
sourceHandle: `outputs/${NodeConnectionType.Main}/0`,
target: nodeB.id,
targetHandle: 'inputs/main/0',
targetHandle: `inputs/${NodeConnectionType.Main}/0`,
};
vi.spyOn(workflowsStore, 'getNodeById').mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB);
@ -280,8 +281,8 @@ describe('useCanvasOperations', () => {
expect(removeConnectionSpy).toHaveBeenCalledWith({
connection: [
{ index: 0, node: nodeA.name, type: 'main' },
{ index: 0, node: nodeB.name, type: 'main' },
{ index: 0, node: nodeA.name, type: NodeConnectionType.Main },
{ index: 0, node: nodeB.name, type: NodeConnectionType.Main },
],
});
});
@ -294,8 +295,8 @@ describe('useCanvasOperations', () => {
.mockImplementation(() => {});
const connection: [IConnection, IConnection] = [
{ node: 'sourceNode', type: 'type', index: 1 },
{ node: 'targetNode', type: 'type', index: 2 },
{ node: 'sourceNode', type: NodeConnectionType.Main, index: 1 },
{ node: 'targetNode', type: NodeConnectionType.Main, index: 2 },
];
canvasOperations.revertDeleteConnection(connection);

View file

@ -24,7 +24,7 @@ export function useCanvasPanning(
/**
* Updates the canvas offset position based on the mouse movement
*/
function panCanvas(e: MouseEvent) {
function panCanvas(e: MouseEvent | TouchEvent) {
const offsetPosition = uiStore.nodeViewOffsetPosition;
const [x, y] = getMousePosition(e);

View file

@ -15,6 +15,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';
import { useTelemetry } from './useTelemetry';
import { useRootStore } from '@/stores/n8nRoot.store';
import { isFullExecutionResponse } from '@/utils/typeGuards';
export const useExecutionDebugging = () => {
const telemetry = useTelemetry();
@ -131,7 +132,7 @@ export const useExecutionDebugging = () => {
telemetry.track('User clicked debug execution button', {
instance_id: useRootStore().instanceId,
exec_status: execution.status,
exec_status: isFullExecutionResponse(execution) ? execution.status : '',
override_pinned_data: pinnableNodes.length === pinnings,
all_exec_data_imported: missingNodeNames.length === 0,
});

View file

@ -271,7 +271,7 @@ export function useNodeHelpers() {
}
}
function updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription): void {
function updateNodeParameterIssues(node: INodeUi, nodeType?: INodeTypeDescription | null): void {
const localNodeType = nodeType ?? nodeTypesStore.getNodeType(node.type, node.typeVersion);
if (localNodeType === null) {

View file

@ -530,6 +530,7 @@ export function usePushConnection({ router }: { router: ReturnType<typeof useRou
const executionData: IExecutionsCurrentSummaryExtended = {
id: pushData.executionId,
finished: false,
status: 'running',
mode: pushData.mode,
startedAt: pushData.startedAt,
retryOf: pushData.retryOf,

View file

@ -39,7 +39,6 @@ import type {
IWorkflowData,
IWorkflowDataUpdate,
IWorkflowDb,
IWorkflowTemplateNode,
TargetItem,
XYPosition,
} from '@/Interface';
@ -308,11 +307,7 @@ function getNodes(): INodeUi[] {
}
// Returns a workflow instance.
function getWorkflow(
nodes: Array<INodeUi | IWorkflowTemplateNode>,
connections: IConnections,
copyData?: boolean,
): Workflow {
function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
return useWorkflowsStore().getWorkflow(nodes, connections, copyData);
}

View file

@ -1,6 +1,7 @@
import { useUIStore } from '@/stores/ui.store';
import type { IFakeDoor } from '@/Interface';
import { FAKE_DOOR_FEATURES } from '@/constants';
import type { BaseTextKey } from '@/plugins/i18n';
export function compileFakeDoorFeatures(): IFakeDoor[] {
const store = useUIStore();
@ -20,7 +21,7 @@ export function compileFakeDoorFeatures(): IFakeDoor[] {
if (loggingFeature) {
loggingFeature.actionBoxTitle += '.cloud';
loggingFeature.linkURL += '&edition=cloud';
loggingFeature.infoText = '';
loggingFeature.infoText = '' as BaseTextKey;
}
return fakeDoorFeatures;

View file

@ -50,7 +50,8 @@ app.mount('#app');
if (!import.meta.env.PROD) {
// Make sure that we get all error messages properly displayed
// as long as we are not in production mode
window.onerror = (message, source, lineno, colno, error) => {
window.onerror = (message, _source, _lineno, _colno, error) => {
// eslint-disable-next-line @typescript-eslint/no-base-to-string
if (message.toString().includes('ResizeObserver')) {
// That error can apparently be ignored and can probably
// not do anything about it anyway

View file

@ -29,6 +29,7 @@ import { useCanvasStore } from '@/stores/canvas.store';
import type { EndpointSpec } from '@jsplumb/common';
import { useDeviceSupport } from 'n8n-design-system';
import type { N8nEndpointLabelLength } from '@/plugins/jsplumb/N8nPlusEndpointType';
import { isValidNodeConnectionType } from '@/utils/typeGuards';
const createAddInputEndpointSpec = (
connectionName: NodeConnectionType,
@ -119,9 +120,9 @@ export const nodeBase = defineComponent({
},
methods: {
__addEndpointTestingData(endpoint: Endpoint, type: string, inputIndex: number) {
if (window?.Cypress && 'canvas' in endpoint.endpoint) {
if (window?.Cypress && 'canvas' in endpoint.endpoint && this.instance) {
const canvas = endpoint.endpoint.canvas;
this.instance.setAttribute(canvas, 'data-endpoint-name', this.data.name);
this.instance.setAttribute(canvas, 'data-endpoint-name', this.data?.name ?? '');
this.instance.setAttribute(canvas, 'data-input-index', inputIndex.toString());
this.instance.setAttribute(canvas, 'data-endpoint-type', type);
}
@ -216,7 +217,11 @@ export const nodeBase = defineComponent({
spacerIndexes,
)[rootTypeIndex];
const scope = NodeViewUtils.getEndpointScope(inputName as NodeConnectionType);
if (!isValidNodeConnectionType(inputName)) {
return;
}
const scope = NodeViewUtils.getEndpointScope(inputName);
const newEndpointData: EndpointOptions = {
uuid: NodeViewUtils.getInputEndpointUUID(this.nodeId, inputName, typeIndex),
@ -252,15 +257,15 @@ export const nodeBase = defineComponent({
};
const endpoint = this.instance?.addEndpoint(
this.$refs[this.data.name] as Element,
this.$refs[this.data?.name ?? ''] as Element,
newEndpointData,
) as Endpoint;
this.__addEndpointTestingData(endpoint, 'input', typeIndex);
if (inputConfiguration.displayName || nodeTypeData.inputNames?.[i]) {
if (inputConfiguration.displayName ?? nodeTypeData.inputNames?.[i]) {
// Apply input names if they got set
endpoint.addOverlay(
NodeViewUtils.getInputNameOverlay(
inputConfiguration.displayName || nodeTypeData.inputNames[i],
inputConfiguration.displayName ?? nodeTypeData.inputNames?.[i] ?? '',
inputName,
inputConfiguration.required,
),
@ -288,7 +293,7 @@ export const nodeBase = defineComponent({
// }
});
if (sortedInputs.length === 0) {
this.instance.manage(this.$refs[this.data.name] as Element);
this.instance?.manage(this.$refs[this.data?.name ?? ''] as Element);
}
},
getSpacerIndexes(
@ -349,6 +354,10 @@ export const nodeBase = defineComponent({
[key: string]: number;
} = {};
if (!this.data) {
return;
}
this.outputs = NodeHelpers.getNodeOutputs(this.workflow, this.data, nodeTypeData) || [];
// TODO: There are still a lot of references of "main" in NodesView and
@ -380,7 +389,7 @@ export const nodeBase = defineComponent({
const endpointLabelLength = getEndpointLabelLength(maxLabelLength);
this.outputs.forEach((value, i) => {
this.outputs.forEach((_value, i) => {
const outputConfiguration = outputConfigurations[i];
const outputName: ConnectionTypes = outputConfiguration.type;
@ -419,7 +428,11 @@ export const nodeBase = defineComponent({
outputsOfSameRootType.length,
)[rootTypeIndex];
const scope = NodeViewUtils.getEndpointScope(outputName as NodeConnectionType);
if (!isValidNodeConnectionType(outputName)) {
return;
}
const scope = NodeViewUtils.getEndpointScope(outputName);
const newEndpointData: EndpointOptions = {
uuid: NodeViewUtils.getOutputEndpointUUID(this.nodeId, outputName, typeIndex),
@ -448,13 +461,17 @@ export const nodeBase = defineComponent({
...this.__getOutputConnectionStyle(outputName, outputConfiguration, nodeTypeData),
};
const endpoint = this.instance.addEndpoint(
this.$refs[this.data.name] as Element,
const endpoint = this.instance?.addEndpoint(
this.$refs[this.data?.name ?? ''] as Element,
newEndpointData,
);
if (!endpoint) {
return;
}
this.__addEndpointTestingData(endpoint, 'output', typeIndex);
if (outputConfiguration.displayName) {
if (outputConfiguration.displayName && isValidNodeConnectionType(outputName)) {
// Apply output names if they got set
const overlaySpec = NodeViewUtils.getOutputNameOverlay(
outputConfiguration.displayName,
@ -514,6 +531,10 @@ export const nodeBase = defineComponent({
plusEndpointData.cssClass = `${plusEndpointData.cssClass} ${outputConfiguration?.category}`;
}
if (!this.instance || !this.data) {
return;
}
const plusEndpoint = this.instance.addEndpoint(
this.$refs[this.data.name] as Element,
plusEndpointData,
@ -556,7 +577,7 @@ export const nodeBase = defineComponent({
};
}
if (!Object.values(NodeConnectionType).includes(connectionType as NodeConnectionType)) {
if (!isValidNodeConnectionType(connectionType)) {
return {};
}
@ -614,13 +635,13 @@ export const nodeBase = defineComponent({
};
}
if (!Object.values(NodeConnectionType).includes(connectionType as NodeConnectionType)) {
if (!isValidNodeConnectionType(connectionType)) {
return {};
}
return createSupplementalConnectionType(connectionType);
},
touchEnd(e: MouseEvent) {
touchEnd(_e: MouseEvent) {
const deviceSupport = useDeviceSupport();
if (deviceSupport.isTouchDevice) {
if (this.uiStore.isActionActive('dragActive')) {
@ -651,7 +672,7 @@ export const nodeBase = defineComponent({
this.$emit('deselectAllNodes');
}
if (this.uiStore.isNodeSelected(this.data.name)) {
if (this.uiStore.isNodeSelected(this.data?.name ?? '')) {
this.$emit('deselectNode', this.name);
} else {
this.$emit('nodeSelected', this.name);

View file

@ -1,7 +1,6 @@
import { defineComponent } from 'vue';
import type { RouteLocation } from 'vue-router';
import { hasPermission } from '@/utils/rbac/permissions';
import type { RouteConfig } from '@/types/router';
import type { PermissionTypeOptions } from '@/types/rbac';
export const userHelpers = defineComponent({
@ -16,7 +15,7 @@ export const userHelpers = defineComponent({
return this.canUserAccessRoute(this.$route);
},
canUserAccessRoute(route: RouteLocation & RouteConfig) {
canUserAccessRoute(route: RouteLocation) {
const middleware = route.meta?.middleware;
const middlewareOptions = route.meta?.middlewareOptions;

View file

@ -6,6 +6,7 @@ import type {
IExecuteData,
INodeTypeData,
} from 'n8n-workflow';
import { NodeConnectionType } from 'n8n-workflow';
import { WorkflowDataProxy } from 'n8n-workflow';
import { createTestWorkflowObject } from '@/__tests__/mocks';
@ -91,7 +92,7 @@ const connections: IConnections = {
[
{
node: 'Function',
type: 'main',
type: NodeConnectionType.Main,
index: 0,
},
],
@ -102,7 +103,7 @@ const connections: IConnections = {
[
{
node: 'Rename',
type: 'main',
type: NodeConnectionType.Main,
index: 0,
},
],
@ -113,7 +114,7 @@ const connections: IConnections = {
[
{
node: 'End',
type: 'main',
type: NodeConnectionType.Main,
index: 0,
},
],

View file

@ -14,8 +14,8 @@ beforeEach(() => {
describe('variablesCompletions', () => {
test('should return completions for $vars prefix', () => {
environmentsStore.variables = [
{ key: 'VAR1', value: 'Value1', id: 1 },
{ key: 'VAR2', value: 'Value2', id: 2 },
{ key: 'VAR1', value: 'Value1', id: '1' },
{ key: 'VAR2', value: 'Value2', id: '2' },
];
const state = EditorState.create({ doc: '$vars.', selection: { anchor: 6 } });
@ -39,7 +39,7 @@ describe('variablesCompletions', () => {
});
test('should escape special characters in matcher', () => {
environmentsStore.variables = [{ key: 'VAR1', value: 'Value1', id: 1 }];
environmentsStore.variables = [{ key: 'VAR1', value: 'Value1', id: '1' }];
const state = EditorState.create({ doc: '$vars.', selection: { anchor: 6 } });
const context = new CompletionContext(state, 6, true);
@ -49,7 +49,7 @@ describe('variablesCompletions', () => {
});
test('should return completions for custom matcher', () => {
environmentsStore.variables = [{ key: 'VAR1', value: 'Value1', id: 1 }];
environmentsStore.variables = [{ key: 'VAR1', value: 'Value1', id: '1' }];
const state = EditorState.create({ doc: '$custom.', selection: { anchor: 8 } });
const context = new CompletionContext(state, 8, true);

View file

@ -9,7 +9,7 @@ import EnterpriseEdition from '@/components/EnterpriseEdition.ee.vue';
import RBAC from '@/components/RBAC.vue';
import ParameterInputList from '@/components/ParameterInputList.vue';
export const GlobalComponentsPlugin: Plugin<{}> = {
export const GlobalComponentsPlugin: Plugin = {
install(app) {
const messageService = useMessage();
@ -18,7 +18,7 @@ export const GlobalComponentsPlugin: Plugin<{}> = {
app.component('ParameterInputList', ParameterInputList);
app.use(ElementPlus);
app.use(N8nPlugin);
app.use(N8nPlugin, {});
// app.use(ElLoading);
// app.use(ElNotification);

View file

@ -169,7 +169,7 @@ export class N8nConnector extends AbstractConnector {
targetGap: number;
overrideTargetEndpoint: Endpoint | null;
overrideTargetEndpoint: Endpoint;
getEndpointOffset?: (e: Endpoint) => number | null;
@ -517,7 +517,7 @@ export class N8nConnector extends AbstractConnector {
}
resetTargetEndpoint() {
this.overrideTargetEndpoint = null;
this.overrideTargetEndpoint = null as unknown as Endpoint;
}
_computeBezier(paintInfo: N8nConnectorPaintGeometry) {

View file

@ -2,7 +2,7 @@ import type { Plugin } from 'vue';
import VueTouchEvents from 'vue3-touch-events';
import { vOnClickOutside } from '@vueuse/components';
export const GlobalDirectivesPlugin: Plugin<{}> = {
export const GlobalDirectivesPlugin: Plugin = {
install(app) {
app.use(VueTouchEvents);
app.directive('on-click-outside', vOnClickOutside);

View file

@ -1,7 +1,7 @@
import type { Plugin } from 'vue';
import axios from 'axios';
import { createI18n } from 'vue-i18n';
import { locale } from 'n8n-design-system';
import { locale, type N8nLocaleTranslateFn } from 'n8n-design-system';
import type { INodeProperties, INodePropertyCollection, INodePropertyOptions } from 'n8n-workflow';
import type { INodeTranslationHeaders } from '@/Interface';
@ -22,6 +22,8 @@ export const i18nInstance = createI18n({
messages: { en: englishBaseText },
});
type BaseTextOptions = { adjustToNumber?: number; interpolate?: Record<string, string | number> };
export class I18nClass {
private baseTextCache = new Map<string, string>();
@ -48,10 +50,7 @@ export class I18nClass {
/**
* Render a string of base text, i.e. a string with a fixed path to the localized value. Optionally allows for [interpolation](https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting) when the localized value contains a string between curly braces.
*/
baseText(
key: BaseTextKey,
options?: { adjustToNumber?: number; interpolate?: Record<string, string | number> },
): string {
baseText(key: BaseTextKey, options?: BaseTextOptions): string {
// Create a unique cache key
const cacheKey = `${key}-${JSON.stringify(options)}`;
@ -438,11 +437,10 @@ export function addHeaders(headers: INodeTranslationHeaders, language: string) {
export const i18n: I18nClass = new I18nClass();
export const I18nPlugin: Plugin<{}> = {
export const I18nPlugin: Plugin = {
async install(app) {
locale.i18n((key: string, options?: { interpolate: Record<string, unknown> }) =>
i18nInstance.global.t(key, options?.interpolate || {}),
);
locale.i18n(((key: string, options?: BaseTextOptions) =>
i18nInstance.global.t(key, options?.interpolate ?? {})) as N8nLocaleTranslateFn);
app.config.globalProperties.$locale = i18n;

View file

@ -166,7 +166,7 @@ function addIcon(icon: IconDefinition) {
library.add(icon);
}
export const FontAwesomePlugin: Plugin<{}> = {
export const FontAwesomePlugin: Plugin = {
install: (app) => {
addIcon(faAngleDoubleLeft);
addIcon(faAngleDown);

View file

@ -32,8 +32,6 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
messageOverlay: Overlay | null;
canvas: HTMLElement | null;
constructor(endpoint: Endpoint, params: N8nPlusEndpointParams) {
super(endpoint, params);
@ -41,7 +39,6 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
this.label = '';
this.stalkOverlay = null;
this.messageOverlay = null;
this.canvas = null;
this.unbindEvents();
this.bindEvents();
@ -111,7 +108,6 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
this[`${fnKey}Class`]('long-stalk');
if (this.label) {
// @ts-expect-error: Overlay interface is missing the `canvas` property
stalkOverlay.canvas.setAttribute('data-label', this.label);
}
}

View file

@ -4,11 +4,13 @@ import * as N8nPlusEndpointRenderer from '@/plugins/jsplumb/N8nPlusEndpointRende
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
import * as N8nAddInputEndpointRenderer from '@/plugins/jsplumb/N8nAddInputEndpointRenderer';
import { N8nAddInputEndpointHandler } from '@/plugins/jsplumb/N8nAddInputEndpointType';
import type { AbstractConnector } from '@jsplumb/core';
import { Connectors, EndpointFactory } from '@jsplumb/core';
import type { Constructable } from '@jsplumb/util';
export const JsPlumbPlugin: Plugin<{}> = {
export const JsPlumbPlugin: Plugin = {
install: () => {
Connectors.register(N8nConnector.type, N8nConnector);
Connectors.register(N8nConnector.type, N8nConnector as Constructable<AbstractConnector>);
N8nPlusEndpointRenderer.register();
EndpointFactory.registerHandler(N8nPlusEndpointHandler);

View file

@ -120,7 +120,7 @@ export class Telemetry {
}
}
page(route: Route) {
page(route: RouteLocation) {
if (this.rudderStack) {
if (route.path === this.previousPath) {
// avoid duplicate requests query is changed for example on search page
@ -128,8 +128,8 @@ export class Telemetry {
}
this.previousPath = route.path;
const pageName = route.name;
let properties: { [key: string]: string } = {};
const pageName = String(route.name);
let properties: Record<string, unknown> = {};
if (route.meta?.telemetry && typeof route.meta.telemetry.getProperties === 'function') {
properties = route.meta.telemetry.getProperties(route);
}
@ -330,7 +330,7 @@ export class Telemetry {
export const telemetry = new Telemetry();
export const TelemetryPlugin: Plugin<{}> = {
export const TelemetryPlugin: Plugin = {
install(app) {
app.config.globalProperties.$telemetry = telemetry;
},

View file

@ -14,7 +14,7 @@ import { useSSOStore } from '@/stores/sso.store';
import { EnterpriseEditionFeature, VIEWS, EDITABLE_CANVAS_VIEWS } from '@/constants';
import { useTelemetry } from '@/composables/useTelemetry';
import { middleware } from '@/utils/rbac/middleware';
import type { RouteConfig, RouterMiddleware } from '@/types/router';
import type { RouterMiddleware } from '@/types/router';
import { initializeCore } from '@/init';
import { tryToParseNumber } from '@/utils/typesUtils';
import { projectsRoutes } from '@/routes/projects.routes';
@ -60,17 +60,17 @@ const WorkerView = async () => await import('./views/WorkerView.vue');
const WorkflowHistory = async () => await import('@/views/WorkflowHistory.vue');
const WorkflowOnboardingView = async () => await import('@/views/WorkflowOnboardingView.vue');
function getTemplatesRedirect(defaultRedirect: VIEWS[keyof VIEWS]) {
function getTemplatesRedirect(defaultRedirect: VIEWS[keyof VIEWS]): { name: string } | false {
const settingsStore = useSettingsStore();
const isTemplatesEnabled: boolean = settingsStore.isTemplatesEnabled;
if (!isTemplatesEnabled) {
return { name: defaultRedirect || VIEWS.NOT_FOUND };
return { name: `${defaultRedirect}` || VIEWS.NOT_FOUND };
}
return false;
}
export const routes = [
export const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/home/workflows',
@ -742,7 +742,7 @@ export const routes = [
},
},
},
] as Array<RouteRecordRaw & RouteConfig>;
];
function withCanvasReadOnlyMeta(route: RouteRecordRaw) {
if (!route.meta) {
@ -759,7 +759,7 @@ function withCanvasReadOnlyMeta(route: RouteRecordRaw) {
const router = createRouter({
history: createWebHistory(import.meta.env.DEV ? '/' : window.BASE_PATH ?? '/'),
scrollBehavior(to: RouteLocationNormalized & RouteConfig, from, savedPosition) {
scrollBehavior(to: RouteLocationNormalized, _, savedPosition) {
// saved position == null means the page is NOT visited from history (back button)
if (savedPosition === null && to.name === VIEWS.TEMPLATES && to.meta?.setScrollPosition) {
// for templates view, reset scroll position in this case
@ -769,7 +769,7 @@ const router = createRouter({
routes: routes.map(withCanvasReadOnlyMeta),
});
router.beforeEach(async (to: RouteLocationNormalized & RouteConfig, from, next) => {
router.beforeEach(async (to: RouteLocationNormalized, from, next) => {
try {
/**
* Initialize application core

View file

@ -1,5 +1,12 @@
import type { Connection, Endpoint, EndpointRepresentation, AbstractConnector, Overlay } from '@jsplumb/core';
import type {
Connection,
Endpoint,
EndpointRepresentation,
AbstractConnector,
Overlay,
} from '@jsplumb/core';
import type { NodeConnectionType } from 'n8n-workflow';
import type { N8nEndpointLabelLength } from '@/plugins/jsplumb/N8nPlusEndpointType';
declare module '@jsplumb/core' {
interface EndpointRepresentation {
@ -27,8 +34,9 @@ declare module '@jsplumb/core' {
nodeName: string;
nodeId: string;
index: number;
nodeType?: string;
totalEndpoints: number;
endpointLabelLength: number;
endpointLabelLength?: N8nEndpointLabelLength;
};
};
}
}

View file

@ -1,3 +1,21 @@
/**
* Modules
*/
declare module 'vue-agile';
/**
* File types
*/
declare module '*.json';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.webp';
declare module 'v3-infinite-loading' {
import { Plugin, DefineComponent } from 'vue';

View file

@ -1,6 +1,16 @@
import 'vue-router';
import type { I18nClass } from '@/plugins/i18n';
import type { Route } from 'vue-router';
import type { Route, RouteLocation } from 'vue-router';
import type { Telemetry } from '@/plugins/telemetry';
import type { VIEWS } from '@/constants';
import type { IPermissions } from '@/Interface';
import type { MiddlewareOptions, RouterMiddlewareType } from '@/types/router';
export {};
/**
* @docs https://vuejs.org/guide/typescript/options-api.html#augmenting-global-properties
*/
declare module 'vue' {
interface ComponentCustomOptions {
@ -17,6 +27,26 @@ declare module 'vue' {
}
/**
* @docs https://vuejs.org/guide/typescript/options-api.html#augmenting-global-properties
* @docs https://router.vuejs.org/guide/advanced/meta
*/
export {};
declare module 'vue-router' {
interface RouteMeta {
nodeView?: boolean;
templatesEnabled?: boolean;
getRedirect?:
| (() => { name: string } | false)
| ((defaultRedirect: VIEWS[keyof VIEWS]) => { name: string } | false);
permissions?: IPermissions;
middleware?: RouterMiddlewareType[];
middlewareOptions?: Partial<MiddlewareOptions>;
telemetry?: {
disabled?: true;
pageCategory?: string;
getProperties?: (route: RouteLocation) => Record<string, unknown>;
};
scrollOffset?: number;
setScrollPosition?: (position: number) => void;
readOnlyCanvas?: boolean;
}
}

View file

@ -1,11 +1,8 @@
import { VNode, ComponentPublicInstance } from 'vue';
import { PartialDeep } from 'type-fest';
import { ExternalHooks } from '@/types/externalHooks';
import type { VNode, ComponentPublicInstance } from 'vue';
import type { PartialDeep } from 'type-fest';
import type { ExternalHooks } from '@/types/externalHooks';
declare module 'markdown-it-link-attributes';
declare module 'markdown-it-emoji';
declare module 'markdown-it-task-lists';
declare module 'vue-agile';
export {};
declare global {
interface ImportMeta {
@ -37,10 +34,3 @@ declare global {
findLast(predicate: (value: T, index: number, obj: T[]) => unknown, thisArg?: any): T;
}
}
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.webp';

View file

@ -19,6 +19,7 @@ import { MANUAL_TRIGGER_NODE_TYPE, START_NODE_TYPE } from '@/constants';
import type {
BeforeStartEventParams,
BrowserJsPlumbInstance,
ConstrainFunction,
DragStopEventParams,
} from '@jsplumb/browser-ui';
import { newInstance } from '@jsplumb/browser-ui';
@ -307,14 +308,14 @@ export const useCanvasStore = defineStore('canvas', () => {
filter: '.node-description, .node-description .node-name, .node-description .node-subtitle',
},
});
jsPlumbInstanceRef.value?.setDragConstrainFunction((pos: PointXY) => {
jsPlumbInstanceRef.value?.setDragConstrainFunction(((pos: PointXY) => {
const isReadOnly = uiStore.isReadOnlyView;
if (isReadOnly) {
// Do not allow to move nodes in readOnly mode
return null;
}
return pos;
});
}) as ConstrainFunction);
}
const jsPlumbInstance = computed(() => jsPlumbInstanceRef.value as BrowserJsPlumbInstance);

View file

@ -236,9 +236,6 @@ export const useCredentialsStore = defineStore(STORES.CREDENTIALS, {
};
}
},
enableOAuthCredential(credential: ICredentialsResponse): void {
// enable oauth event to track change between modals
},
async fetchCredentialTypes(forceFetch: boolean): Promise<void> {
if (this.allCredentialTypes.length > 0 && !forceFetch) {
return;

View file

@ -201,7 +201,7 @@ export const useLogStreamingStore = defineStore('logStreaming', {
return false;
}
},
async sendTestMessage(destination: MessageEventBusDestinationOptions) {
async sendTestMessage(destination: MessageEventBusDestinationOptions): Promise<boolean> {
if (!hasDestinationId(destination)) {
return false;
}

View file

@ -233,6 +233,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
isDragging: false,
type: '',
data: '',
dimensions: null,
activeTarget: null,
};
},

View file

@ -155,7 +155,7 @@ export const usePostHog = defineStore('posthog', () => {
trackExperimentsDebounced(featureFlags.value);
} else {
// depend on client side evaluation if serverside evaluation fails
window.posthog?.onFeatureFlags?.((keys: string[], map: FeatureFlags) => {
window.posthog?.onFeatureFlags?.((_, map: FeatureFlags) => {
featureFlags.value = map;
// must be debounced because it is called multiple times by posthog

View file

@ -32,6 +32,7 @@ export const useRBACStore = defineStore(STORES.RBAC, () => {
license: {},
logStreaming: {},
saml: {},
securityAudit: {},
});
function addGlobalRole(role: IRole) {

View file

@ -274,7 +274,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, {
this.setSettings(settings);
this.settings.communityNodesEnabled = settings.communityNodesEnabled;
this.setAllowedModules(settings.allowedModules as { builtIn?: string; external?: string });
this.setAllowedModules(settings.allowedModules);
this.setSaveDataErrorExecution(settings.saveDataErrorExecution);
this.setSaveDataSuccessExecution(settings.saveDataSuccessExecution);
this.setSaveManualExecutions(settings.saveManualExecutions);

View file

@ -63,7 +63,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
return (id: string): null | ITemplatesCollection => this.collections[id];
},
getCategoryById() {
return (id: string): null | ITemplatesCategory => this.categories[id];
return (id: string): null | ITemplatesCategory => this.categories[id as unknown as number];
},
getSearchedCollections() {
return (query: ITemplatesQuery) => {
@ -121,7 +121,7 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
* Constructs URLSearchParams object based on the default parameters for the template repository
* and provided additional parameters
*/
websiteTemplateRepositoryParameters(roleOverride?: string) {
websiteTemplateRepositoryParameters(_roleOverride?: string) {
const rootStore = useRootStore();
const userStore = useUsersStore();
const workflowsStore = useWorkflowsStore();
@ -131,8 +131,12 @@ export const useTemplatesStore = defineStore(STORES.TEMPLATES, {
utm_n8n_version: rootStore.versionCli,
utm_awc: String(workflowsStore.activeWorkflows.length),
};
const userRole: string | undefined =
userStore.currentUserCloudInfo?.role ?? userStore.currentUser?.personalizationAnswers?.role;
const userRole: string | null | undefined =
userStore.currentUserCloudInfo?.role ??
(userStore.currentUser?.personalizationAnswers &&
'role' in userStore.currentUser.personalizationAnswers
? userStore.currentUser.personalizationAnswers.role
: undefined);
if (userRole) {
defaultParameters.utm_user_role = userRole;

View file

@ -2,9 +2,7 @@ import type {
NavigationGuardNext,
NavigationGuardWithThis,
RouteLocationNormalized,
RouteLocation,
} from 'vue-router';
import type { IPermissions } from '@/Interface';
import type {
AuthenticatedPermissionOptions,
CustomPermissionOptions,
@ -32,24 +30,6 @@ export type MiddlewareOptions = {
role: RolePermissionOptions;
};
export interface RouteConfig {
meta: {
nodeView?: boolean;
templatesEnabled?: boolean;
getRedirect?: () => { name: string } | false;
permissions?: IPermissions;
middleware?: RouterMiddlewareType[];
middlewareOptions?: Partial<MiddlewareOptions>;
telemetry?: {
disabled?: true;
getProperties: (route: RouteLocation) => object;
};
scrollOffset?: number;
setScrollPosition?: (position: number) => void;
readOnlyCanvas?: boolean;
};
}
export type RouterMiddlewareReturnType = ReturnType<NavigationGuardWithThis<undefined>>;
export interface RouterMiddleware<RouterMiddlewareOptions = {}> {

View file

@ -5,6 +5,7 @@ import type {
ITemplatesNode,
IVersionNode,
NodeAuthenticationOption,
SimplifiedNodeType,
} from '@/Interface';
import {
CORE_NODES_CATEGORY,
@ -451,21 +452,21 @@ export const getThemedValue = <T extends string>(
};
export const getNodeIcon = (
nodeType: INodeTypeDescription | IVersionNode,
nodeType: INodeTypeDescription | SimplifiedNodeType | IVersionNode,
theme: AppliedThemeOption = 'light',
): string | null => {
return getThemedValue(nodeType.icon, theme);
};
export const getNodeIconUrl = (
nodeType: INodeTypeDescription | IVersionNode,
nodeType: INodeTypeDescription | SimplifiedNodeType | IVersionNode,
theme: AppliedThemeOption = 'light',
): string | null => {
return getThemedValue(nodeType.iconUrl, theme);
};
export const getBadgeIconUrl = (
nodeType: INodeTypeDescription,
nodeType: INodeTypeDescription | SimplifiedNodeType,
theme: AppliedThemeOption = 'light',
): string | null => {
return getThemedValue(nodeType.badgeIconUrl, theme);

View file

@ -1,6 +1,6 @@
import { isNumber, isValidNodeConnectionType } from '@/utils/typeGuards';
import { NODE_OUTPUT_DEFAULT_KEY, STICKY_NODE_TYPE } from '@/constants';
import type { EndpointMeta, EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface';
import type { EndpointStyle, IBounds, INodeUi, XYPosition } from '@/Interface';
import type { ArrayAnchorSpec, ConnectorSpec, OverlaySpec, PaintStyle } from '@jsplumb/common';
import type { Connection, Endpoint, SelectOptions } from '@jsplumb/core';
import { N8nConnector } from '@/plugins/connectors/N8nCustomConnector';
@ -73,7 +73,7 @@ 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 & EndpointMeta) {
getEndpointOffset(endpoint: Endpoint) {
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;
@ -320,7 +320,7 @@ export const getOutputNameOverlay = (
options: {
id: OVERLAY_OUTPUT_NAME_LABEL,
visible: true,
create: (ep: Endpoint & EndpointMeta) => {
create: (ep: Endpoint) => {
const label = document.createElement('div');
label.innerHTML = labelText;
label.classList.add('node-output-endpoint-label');
@ -1120,7 +1120,7 @@ export const getPlusEndpoint = (
): Endpoint | undefined => {
const endpoints = getJSPlumbEndpoints(node, instance);
return endpoints.find(
(endpoint: Endpoint & EndpointMeta) =>
(endpoint: Endpoint) =>
endpoint.endpoint.type === 'N8nPlus' && endpoint?.__meta?.index === outputIndex,
);
};

View file

@ -11,6 +11,7 @@ describe('templateTransforms', () => {
getNodeType: vitest.fn(),
};
const node = newWorkflowTemplateNode({
id: 'twitter',
type: 'n8n-nodes-base.twitter',
credentials: {
twitterOAuth1Api: 'old1',
@ -40,6 +41,7 @@ describe('templateTransforms', () => {
getNodeType: vitest.fn(),
};
const node = newWorkflowTemplateNode({
id: 'twitter',
type: 'n8n-nodes-base.twitter',
});
const toReplaceWith = {

View file

@ -7,6 +7,7 @@ export const newWorkflowTemplateNode = ({
type,
...optionalOpts
}: Pick<IWorkflowTemplateNode, 'type'> &
Pick<IWorkflowTemplateNode, 'id'> &
Partial<IWorkflowTemplateNode>): IWorkflowTemplateNode => ({
type,
name: faker.commerce.productName(),
@ -306,7 +307,6 @@ export const fullSaveEmailAttachmentsToNextCloudTemplate = {
export const fullCreateApiEndpointTemplate = {
id: 1750,
name: 'Creating an API endpoint',
recentViews: 9899,
totalViews: 13265,
createdAt: '2022-07-06T14:45:19.659Z',
description:
@ -393,7 +393,6 @@ export const fullCreateApiEndpointTemplate = {
},
},
},
lastUpdatedBy: 1,
workflowInfo: {
nodeCount: 2,
nodeTypes: {},

View file

@ -5,7 +5,7 @@ import type {
TriggerPanelDefinition,
} from 'n8n-workflow';
import { nodeConnectionTypes } from 'n8n-workflow';
import type { ICredentialsResponse, NewCredentialsModal } from '@/Interface';
import type { IExecutionResponse, ICredentialsResponse, NewCredentialsModal } from '@/Interface';
import type { jsPlumbDOMElement } from '@jsplumb/browser-ui';
import type { Connection } from '@jsplumb/core';
@ -73,3 +73,9 @@ export function isTriggerPanelObject(
): triggerPanel is TriggerPanelDefinition {
return triggerPanel !== undefined && typeof triggerPanel === 'object' && triggerPanel !== null;
}
export function isFullExecutionResponse(
execution: IExecutionResponse | null,
): execution is IExecutionResponse {
return !!execution && 'status' in execution;
}

View file

@ -63,6 +63,7 @@
import type { ICredentialsResponse, ICredentialTypeMap } from '@/Interface';
import { defineComponent } from 'vue';
import type { IResource } from '@/components/layouts/ResourcesListLayout.vue';
import ResourcesListLayout from '@/components/layouts/ResourcesListLayout.vue';
import CredentialCard from '@/components/CredentialCard.vue';
import type { ICredentialType } from 'n8n-workflow';
@ -106,8 +107,14 @@ export default defineComponent({
useExternalSecretsStore,
useProjectsStore,
),
allCredentials(): ICredentialsResponse[] {
return this.credentialsStore.allCredentials;
allCredentials(): IResource[] {
return this.credentialsStore.allCredentials.map((credential) => ({
id: credential.id,
name: credential.name,
value: '',
updatedAt: credential.updatedAt,
createdAt: credential.createdAt,
}));
},
allCredentialTypes(): ICredentialType[] {
return this.credentialsStore.allCredentialTypes;

View file

@ -2178,7 +2178,11 @@ export default defineComponent({
}
}
return await this.importWorkflowData(workflowData!, 'paste', false);
if (!workflowData) {
return;
}
return await this.importWorkflowData(workflowData, 'paste', false);
}
},
@ -2205,7 +2209,7 @@ export default defineComponent({
// Imports the given workflow data into the current workflow
async importWorkflowData(
workflowData: IWorkflowToShare,
workflowData: IWorkflowDataUpdate,
source: string,
importTags = true,
): Promise<void> {
@ -2342,7 +2346,7 @@ export default defineComponent({
}
},
removeUnknownCredentials(workflow: IWorkflowToShare) {
removeUnknownCredentials(workflow: IWorkflowDataUpdate) {
if (!workflow?.nodes) return;
for (const node of workflow.nodes) {

View file

@ -93,6 +93,7 @@ describe('SetupWorkflowFromTemplateView store', () => {
const templatesStore = useTemplatesStore();
const workflow = testData.newFullOneNodeTemplate({
id: 'workflow',
name: 'Test',
type: 'n8n-nodes-base.httpRequest',
typeVersion: 1,

View file

@ -15,6 +15,7 @@ const objToMap = <TKey extends string, T>(obj: Record<TKey, T>) => {
describe('useCredentialSetupState', () => {
const nodesByName = {
Twitter: {
id: 'twitter',
name: 'Twitter',
type: 'n8n-nodes-base.twitter',
position: [720, -220],
@ -58,12 +59,14 @@ describe('useCredentialSetupState', () => {
it('returns credentials grouped when the credential names are the same', () => {
const [node1, node2] = [
newWorkflowTemplateNode({
id: 'twitter',
type: 'n8n-nodes-base.twitter',
credentials: {
twitterOAuth1Api: 'credential',
},
}) as IWorkflowTemplateNodeWithCredentials,
newWorkflowTemplateNode({
id: 'telegram',
type: 'n8n-nodes-base.telegram',
credentials: {
telegramApi: 'credential',

View file

@ -122,6 +122,16 @@ export default defineComponent({
TemplateList,
TemplatesView,
},
beforeRouteLeave(_to, _from, next) {
const contentArea = document.getElementById('content');
if (contentArea) {
// When leaving this page, store current scroll position in route data
this.$route.meta?.setScrollPosition?.(contentArea.scrollTop);
}
this.trackSearch();
next();
},
setup() {
const { callDebounced } = useDebounce();
@ -406,21 +416,6 @@ export default defineComponent({
}, 0);
},
},
beforeRouteLeave(to, from, next) {
const contentArea = document.getElementById('content');
if (contentArea) {
// When leaving this page, store current scroll position in route data
if (
this.$route.meta?.setScrollPosition &&
typeof this.$route.meta.setScrollPosition === 'function'
) {
this.$route.meta.setScrollPosition(contentArea.scrollTop);
}
}
this.trackSearch();
next();
},
});
</script>

View file

@ -122,7 +122,7 @@ const resourceToEnvironmentVariable = (data: IResource): EnvironmentVariable =>
return {
id: data.id,
key: data.name,
value: data.value,
value: 'value' in data ? data.value : '',
};
};

View file

@ -12,7 +12,7 @@ import { createUser } from '@/__tests__/data/users';
import { createProjectListItem } from '@/__tests__/data/projects';
import { useRBACStore } from '@/stores/rbac.store';
import { DELETE_USER_MODAL_KEY } from '@/constants';
import { expect } from 'vitest';
import * as usersApi from '@/api/users';
const wrapperComponentWithModal = {
components: { SettingsUsersView, ModalRoot, DeleteUserModal },
@ -34,31 +34,34 @@ const loggedInUser = createUser();
const users = Array.from({ length: 3 }, createUser);
const personalProjects = Array.from({ length: 3 }, createProjectListItem);
let pinia: ReturnType<typeof createPinia>;
let projectsStore: ReturnType<typeof useProjectsStore>;
let usersStore: ReturnType<typeof useUsersStore>;
let rbacStore: ReturnType<typeof useRBACStore>;
describe('SettingsUsersView', () => {
beforeEach(() => {
setActivePinia(createPinia());
pinia = createPinia();
setActivePinia(pinia);
projectsStore = useProjectsStore();
usersStore = useUsersStore();
rbacStore = useRBACStore();
vi.spyOn(rbacStore, 'hasScope').mockReturnValue(true);
vi.spyOn(usersStore, 'fetchUsers').mockImplementation(async () => await Promise.resolve());
vi.spyOn(usersApi, 'getUsers').mockResolvedValue(users);
vi.spyOn(usersStore, 'allUsers', 'get').mockReturnValue(users);
vi.spyOn(usersStore, 'getUserById', 'get').mockReturnValue(() => loggedInUser);
vi.spyOn(projectsStore, 'getAllProjects').mockImplementation(
async () => await Promise.resolve(),
);
vi.spyOn(projectsStore, 'personalProjects', 'get').mockReturnValue(personalProjects);
usersStore.currentUserId = loggedInUser.id;
});
it('should show confirmation modal before deleting user and delete with transfer', async () => {
const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {});
const { getByTestId } = renderComponent();
const { getByTestId } = renderComponent({ pinia });
const userListItem = getByTestId(`user-list-item-${users[0].email}`);
expect(userListItem).toBeInTheDocument();
@ -98,7 +101,7 @@ describe('SettingsUsersView', () => {
it('should show confirmation modal before deleting user and delete without transfer', async () => {
const deleteUserSpy = vi.spyOn(usersStore, 'deleteUser').mockImplementation(async () => {});
const { getByTestId } = renderComponent();
const { getByTestId } = renderComponent({ pinia });
const userListItem = getByTestId(`user-list-item-${users[0].email}`);
expect(userListItem).toBeInTheDocument();

View file

@ -14,11 +14,7 @@
"types": [
"vitest/globals",
"unplugin-icons/types/vue",
"./src/shims.d.ts",
"./src/shims-vue.d.ts",
"./src/v3-infinite-loading.d.ts",
"../workflow/src/types.d.ts",
"../design-system/src/shims-markdown-it.d.ts"
"../design-system/src/shims-modules.d.ts"
],
"paths": {
"@/*": ["./src/*"],

View file

@ -77,8 +77,12 @@ const plugins = [
}),
vue(),
];
if (process.env.ENABLE_TYPE_CHECKING === 'true') {
plugins.push(checker({ vueTsc: true }));
if (!process.env.VITEST) {
plugins.push({
...checker({ vueTsc: true }),
apply: 'build'
});
}
const { SENTRY_AUTH_TOKEN: authToken, RELEASE: release } = process.env;

View file

@ -116,21 +116,21 @@ export interface IUser {
lastName: string;
}
export type ProjectSharingData = {
id: string;
name: string | null;
type: 'personal' | 'team' | 'public';
createdAt: string;
updatedAt: string;
};
export interface ICredentialsDecrypted {
id: string;
name: string;
type: string;
data?: ICredentialDataDecryptedObject;
homeProject?: {
id: string;
name: string | null;
type: 'personal' | 'team' | 'public';
};
sharedWithProjects?: Array<{
id: string;
name: string | null;
type: 'personal' | 'team' | 'public';
}>;
homeProject?: ProjectSharingData;
sharedWithProjects?: ProjectSharingData[];
}
export interface ICredentialsEncrypted {
@ -340,7 +340,13 @@ export interface ICredentialData {
}
// The encrypted credentials which the nodes can access
export type CredentialInformation = string | number | boolean | IDataObject | IDataObject[];
export type CredentialInformation =
| string
| string[]
| number
| boolean
| IDataObject
| IDataObject[];
// The encrypted credentials which the nodes can access
export interface ICredentialDataDecryptedObject {
@ -1530,7 +1536,7 @@ export interface INodeIssueObjectProperty {
export interface INodeIssueData {
node: string;
type: INodeIssueTypes;
value: boolean | string | string[] | INodeIssueObjectProperty;
value: null | boolean | string | string[] | INodeIssueObjectProperty;
}
export interface INodeIssues {
@ -1571,7 +1577,7 @@ export interface INodeTypeBaseDescription {
icon?: Themed<Icon>;
iconColor?: NodeIconColor;
iconUrl?: Themed<string>;
badgeIconUrl?: string;
badgeIconUrl?: Themed<string>;
group: string[];
description: string;
documentationUrl?: string;

View file

@ -21,6 +21,13 @@ export const enum MessageEventBusDestinationTypeNames {
syslog = '$$MessageEventBusDestinationSyslog',
}
export const messageEventBusDestinationTypeNames = [
MessageEventBusDestinationTypeNames.abstract,
MessageEventBusDestinationTypeNames.webhook,
MessageEventBusDestinationTypeNames.sentry,
MessageEventBusDestinationTypeNames.syslog,
];
// ===============================
// Event Message Interfaces
// ===============================