From ef79b03f38460a20658c62fd35dbcaf6d266582f Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 21 Apr 2023 11:25:39 +0200 Subject: [PATCH 001/110] feat(editor): Version control paywall (WIP) (#6030) * feat(editor): Version control paywall (WIP) * fix(editor): remove version control docs link --- .../src/plugins/i18n/locales/en.json | 3 + .../editor-ui/src/stores/versionControl.ts | 16 +++++ .../src/views/SettingsVersionControl.vue | 31 +++++++++- .../__tests__/SettingsVersionControl.test.ts | 60 +++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 packages/editor-ui/src/stores/versionControl.ts create mode 100644 packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index e40f8fe355ba7..0e7454c128030 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1292,6 +1292,9 @@ "settings.usageAndPlan.desktop.title": "Upgrade to n8n Cloud for the full experience", "settings.usageAndPlan.desktop.description": "Cloud plans allow you to collaborate with teammates. Plus you don’t need to leave this app open all the time for your workflows to run.", "settings.versionControl.title": "Version Control", + "settings.versionControl.actionBox.title": "Available on Enterprise plan", + "settings.versionControl.actionBox.description": "Use Version Control to connect your instance to an external Git repository to backup and track changes made to your workflows, variables, and credentials. With Version Control you can also sync instances across multiple environments (development, production...).", + "settings.versionControl.actionBox.buttonText": "See plans", "showMessage.cancel": "@:_reusableBaseText.cancel", "showMessage.ok": "OK", "showMessage.showDetails": "Show Details", diff --git a/packages/editor-ui/src/stores/versionControl.ts b/packages/editor-ui/src/stores/versionControl.ts new file mode 100644 index 0000000000000..315d20445570a --- /dev/null +++ b/packages/editor-ui/src/stores/versionControl.ts @@ -0,0 +1,16 @@ +import { computed } from 'vue'; +import { defineStore } from 'pinia'; +import { EnterpriseEditionFeature } from '@/constants'; +import { useSettingsStore } from '@/stores/settings'; + +export const useVersionControlStore = defineStore('versionControl', () => { + const settingsStore = useSettingsStore(); + + const isEnterpriseVersionControlEnabled = computed(() => + settingsStore.isEnterpriseFeatureEnabled(EnterpriseEditionFeature.VersionControl), + ); + + return { + isEnterpriseVersionControlEnabled, + }; +}); diff --git a/packages/editor-ui/src/views/SettingsVersionControl.vue b/packages/editor-ui/src/views/SettingsVersionControl.vue index ca0a897e6c399..4c6906c6a5741 100644 --- a/packages/editor-ui/src/views/SettingsVersionControl.vue +++ b/packages/editor-ui/src/views/SettingsVersionControl.vue @@ -1,11 +1,40 @@ - + diff --git a/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts b/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts new file mode 100644 index 0000000000000..c3bec841cc9ce --- /dev/null +++ b/packages/editor-ui/src/views/__tests__/SettingsVersionControl.test.ts @@ -0,0 +1,60 @@ +import { PiniaVuePlugin } from 'pinia'; +import { render } from '@testing-library/vue'; +import { createTestingPinia } from '@pinia/testing'; +import { merge } from 'lodash-es'; +import { STORES } from '@/constants'; +import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { i18n } from '@/plugins/i18n'; +import SettingsVersionControl from '@/views/SettingsVersionControl.vue'; +import { useVersionControlStore } from '@/stores/versionControl'; + +let pinia: ReturnType; +let versionControlStore: ReturnType; + +const renderComponent = (renderOptions: Parameters[1] = {}) => + render( + SettingsVersionControl, + merge( + { + pinia, + i18n, + }, + renderOptions, + ), + (vue) => { + vue.use(PiniaVuePlugin); + }, + ); + +describe('SettingsSso', () => { + beforeEach(() => { + pinia = createTestingPinia({ + initialState: { + [STORES.SETTINGS]: { + settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings), + }, + }, + }); + versionControlStore = useVersionControlStore(pinia); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it('should render paywall state when there is no license', () => { + const { getByTestId, queryByTestId } = renderComponent(); + + expect(queryByTestId('version-control-content-licensed')).not.toBeInTheDocument(); + expect(getByTestId('version-control-content-unlicensed')).toBeInTheDocument(); + }); + + it('should render licensed content', () => { + vi.spyOn(versionControlStore, 'isEnterpriseVersionControlEnabled', 'get').mockReturnValue(true); + + const { getByTestId, queryByTestId } = renderComponent(); + + expect(getByTestId('version-control-content-licensed')).toBeInTheDocument(); + expect(queryByTestId('version-control-content-unlicensed')).not.toBeInTheDocument(); + }); +}); From 444ed1bf0e613f748a9ad4a7db3798bf312d37ab Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Fri, 21 Apr 2023 12:08:24 +0200 Subject: [PATCH 002/110] fix(core): Add breaking change record for domain and url matching (no-changelog) (#6048) * fix(core): Add breaking change record for domain and url matching * Correct version --- packages/cli/BREAKING-CHANGES.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md index 47bb59f918d9e..f260be6fa2fb5 100644 --- a/packages/cli/BREAKING-CHANGES.md +++ b/packages/cli/BREAKING-CHANGES.md @@ -2,6 +2,17 @@ This list shows all the versions which include breaking changes and how to upgrade. +## 0.226.0 + +### What changed? + +The `extractDomain` and `isDomain` are now also matching localhost, domains without protocol and domains with query parameters. +The `extractUrl` and `isUrl` are additionally also matching localhost and domains with query parameters. + +### When is action necessary? + +If you're using the `extractDomain` or `isDomain` functions and expect them to not match localhost, domains without protocol and domains with query parameters. + ## 0.223.0 ### What changed? From da319250838b24ccb5a9265aca80d80d19296bc1 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Fri, 21 Apr 2023 13:08:16 +0200 Subject: [PATCH 003/110] refactor(core): Sort variables files under variables folder (#6051) sort variables files under variables folder --- packages/cli/src/Server.ts | 4 ++-- .../versionControl/versionControlHelper.ee.ts | 18 ------------------ .../{ => variables}/enviromentHelpers.ts | 0 .../{ => variables}/variables.controller.ee.ts | 0 .../{ => variables}/variables.controller.ts | 0 .../{ => variables}/variables.service.ee.ts | 0 .../{ => variables}/variables.service.ts | 0 packages/cli/test/integration/shared/utils.ts | 2 +- 8 files changed, 3 insertions(+), 21 deletions(-) delete mode 100644 packages/cli/src/environment/versionControl/versionControlHelper.ee.ts rename packages/cli/src/environments/{ => variables}/enviromentHelpers.ts (100%) rename packages/cli/src/environments/{ => variables}/variables.controller.ee.ts (100%) rename packages/cli/src/environments/{ => variables}/variables.controller.ts (100%) rename packages/cli/src/environments/{ => variables}/variables.service.ee.ts (100%) rename packages/cli/src/environments/{ => variables}/variables.service.ts (100%) diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index bdce01787f3bd..49320cfbc2537 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -156,9 +156,9 @@ import { import { getSamlLoginLabel, isSamlLoginEnabled, isSamlLicensed } from './sso/saml/samlHelpers'; import { SamlController } from './sso/saml/routes/saml.controller.ee'; import { SamlService } from './sso/saml/saml.service.ee'; -import { variablesController } from './environments/variables.controller'; +import { variablesController } from './environments/variables/variables.controller'; import { LdapManager } from './Ldap/LdapManager.ee'; -import { getVariablesLimit, isVariablesEnabled } from '@/environments/enviromentHelpers'; +import { getVariablesLimit, isVariablesEnabled } from '@/environments/variables/enviromentHelpers'; import { getCurrentAuthenticationMethod } from './sso/ssoHelpers'; import { isVersionControlLicensed } from '@/environments/versionControl/versionControlHelper'; import { VersionControlService } from '@/environments/versionControl/versionControl.service.ee'; diff --git a/packages/cli/src/environment/versionControl/versionControlHelper.ee.ts b/packages/cli/src/environment/versionControl/versionControlHelper.ee.ts deleted file mode 100644 index b3857120db144..0000000000000 --- a/packages/cli/src/environment/versionControl/versionControlHelper.ee.ts +++ /dev/null @@ -1,18 +0,0 @@ -import Container from 'typedi'; -import { License } from '../../License'; -import { generateKeyPairSync } from 'crypto'; - -export function isVersionControlEnabled() { - const license = Container.get(License); - return license.isVersionControlLicensed(); -} - -export async function generateSshKeyPair() { - const keyPair = generateKeyPairSync('ed25519', { - privateKeyEncoding: { format: 'pem', type: 'pkcs8' }, - publicKeyEncoding: { format: 'pem', type: 'spki' }, - }); - - console.log(keyPair.privateKey); - console.log(keyPair.publicKey); -} diff --git a/packages/cli/src/environments/enviromentHelpers.ts b/packages/cli/src/environments/variables/enviromentHelpers.ts similarity index 100% rename from packages/cli/src/environments/enviromentHelpers.ts rename to packages/cli/src/environments/variables/enviromentHelpers.ts diff --git a/packages/cli/src/environments/variables.controller.ee.ts b/packages/cli/src/environments/variables/variables.controller.ee.ts similarity index 100% rename from packages/cli/src/environments/variables.controller.ee.ts rename to packages/cli/src/environments/variables/variables.controller.ee.ts diff --git a/packages/cli/src/environments/variables.controller.ts b/packages/cli/src/environments/variables/variables.controller.ts similarity index 100% rename from packages/cli/src/environments/variables.controller.ts rename to packages/cli/src/environments/variables/variables.controller.ts diff --git a/packages/cli/src/environments/variables.service.ee.ts b/packages/cli/src/environments/variables/variables.service.ee.ts similarity index 100% rename from packages/cli/src/environments/variables.service.ee.ts rename to packages/cli/src/environments/variables/variables.service.ee.ts diff --git a/packages/cli/src/environments/variables.service.ts b/packages/cli/src/environments/variables/variables.service.ts similarity index 100% rename from packages/cli/src/environments/variables.service.ts rename to packages/cli/src/environments/variables/variables.service.ts diff --git a/packages/cli/test/integration/shared/utils.ts b/packages/cli/test/integration/shared/utils.ts index b3dcd3d2f0ab8..3625caf661a84 100644 --- a/packages/cli/test/integration/shared/utils.ts +++ b/packages/cli/test/integration/shared/utils.ts @@ -73,7 +73,7 @@ import { v4 as uuid } from 'uuid'; import { InternalHooks } from '@/InternalHooks'; import { LoadNodesAndCredentials } from '@/LoadNodesAndCredentials'; import { PostHogClient } from '@/posthog'; -import { variablesController } from '@/environments/variables.controller'; +import { variablesController } from '@/environments/variables/variables.controller'; import { LdapManager } from '@/Ldap/LdapManager.ee'; import { handleLdapInit } from '@/Ldap/helpers'; import { Push } from '@/push'; From 0e93fe064ecd45bcf764f82595914b0a7e0da3dd Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:23:15 +0300 Subject: [PATCH 004/110] refactor(core): Forbid raw enums (no-changelog) --- packages/@n8n_io/eslint-config/base.js | 12 +++++++++++ packages/cli/src/CurlConverterHelper.ts | 2 +- packages/cli/src/constants.ts | 4 ++-- .../databases/entities/WorkflowStatistics.ts | 2 +- packages/cli/src/events/WorkflowStatistics.ts | 2 +- packages/editor-ui/src/Interface.ts | 2 +- packages/editor-ui/src/constants.ts | 14 ++++++------- packages/editor-ui/src/models/history.ts | 2 +- packages/editor-ui/src/permissions.ts | 2 +- packages/nodes-base/.eslintrc.js | 1 - .../nodes-base/nodes/Aws/DynamoDB/types.d.ts | 2 +- .../nodes-base/nodes/Clockify/CommonDtos.ts | 4 ++-- .../nodes/Clockify/EntryTypeEnum.ts | 2 +- .../nodes/Clockify/ProjectInterfaces.ts | 4 ++-- .../nodes-base/nodes/Clockify/UserDtos.ts | 2 +- .../nodes/Clockify/WorkpaceInterfaces.ts | 8 +++---- .../nodes/Cortex/AnalyzerInterface.ts | 6 +++--- .../nodes/Formstack/GenericFunctions.ts | 2 +- .../nodes/Freshdesk/Freshdesk.node.ts | 6 +++--- .../nodes/Google/Chat/MessageInterface.ts | 2 +- .../Sheet/v2/helpers/GoogleSheets.types.ts | 10 ++++----- .../nodes/ItemLists/V1/summarize.operation.ts | 20 +++++++++--------- .../nodes/ItemLists/V2/summarize.operation.ts | 21 ++++++++++--------- .../nodes/Mailchimp/Mailchimp.node.ts | 2 +- .../nodes/PayPal/PaymentInteface.ts | 4 ++-- .../nodes/SendInBlue/GenericFunctions.ts | 4 ++-- .../TheHive/interfaces/AlertInterface.ts | 4 ++-- .../nodes/TheHive/interfaces/CaseInterface.ts | 6 +++--- .../nodes/TheHive/interfaces/LogInterface.ts | 2 +- .../TheHive/interfaces/ObservableInterface.ts | 4 ++-- .../nodes/TheHive/interfaces/TaskInterface.ts | 2 +- .../nodes/Todoist/v1/OperationHandler.ts | 2 +- .../nodes-base/nodes/Todoist/v1/Service.ts | 21 +++++++++---------- .../nodes/Todoist/v1/TodoistV1.node.ts | 11 ++++------ .../nodes/Todoist/v2/OperationHandler.ts | 2 +- .../nodes-base/nodes/Todoist/v2/Service.ts | 21 +++++++++---------- .../nodes/Todoist/v2/TodoistV2.node.ts | 11 ++++------ packages/workflow/src/Interfaces.ts | 2 +- packages/workflow/src/MessageEventBus.ts | 4 ++-- 39 files changed, 119 insertions(+), 115 deletions(-) diff --git a/packages/@n8n_io/eslint-config/base.js b/packages/@n8n_io/eslint-config/base.js index 2f673ece0b39a..4242429369be4 100644 --- a/packages/@n8n_io/eslint-config/base.js +++ b/packages/@n8n_io/eslint-config/base.js @@ -397,6 +397,18 @@ const config = (module.exports = { }, ], + /** + * https://www.typescriptlang.org/docs/handbook/enums.html#const-enums + */ + 'no-restricted-syntax': [ + 'error', + { + selector: 'TSEnumDeclaration:not([const=true])', + message: + 'Do not declare raw enums as it leads to runtime overhead. Use const enum instead. See https://www.typescriptlang.org/docs/handbook/enums.html#const-enums', + }, + ], + // ---------------------------------- // import // ---------------------------------- diff --git a/packages/cli/src/CurlConverterHelper.ts b/packages/cli/src/CurlConverterHelper.ts index 3035ada4c4f46..06da059030549 100644 --- a/packages/cli/src/CurlConverterHelper.ts +++ b/packages/cli/src/CurlConverterHelper.ts @@ -80,7 +80,7 @@ type HttpNodeHeaders = Pick; -enum ContentTypes { +const enum ContentTypes { applicationJson = 'application/json', applicationFormUrlEncoded = 'application/x-www-form-urlencoded', applicationMultipart = 'multipart/form-data', diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index 913f37381fea2..6d6ebab38589e 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -68,7 +68,7 @@ export const WORKFLOW_REACTIVATE_MAX_TIMEOUT = 24 * 60 * 60 * 1000; // 1 day export const SETTINGS_LICENSE_CERT_KEY = 'license.cert'; -export enum LICENSE_FEATURES { +export const enum LICENSE_FEATURES { SHARING = 'feat:sharing', LDAP = 'feat:ldap', SAML = 'feat:saml', @@ -78,7 +78,7 @@ export enum LICENSE_FEATURES { VERSION_CONTROL = 'feat:versionControl', } -export enum LICENSE_QUOTAS { +export const enum LICENSE_QUOTAS { TRIGGER_LIMIT = 'quota:activeWorkflows', VARIABLES_LIMIT = 'quota:maxVariables', } diff --git a/packages/cli/src/databases/entities/WorkflowStatistics.ts b/packages/cli/src/databases/entities/WorkflowStatistics.ts index 1d3de6316d832..5181bb257c54a 100644 --- a/packages/cli/src/databases/entities/WorkflowStatistics.ts +++ b/packages/cli/src/databases/entities/WorkflowStatistics.ts @@ -3,7 +3,7 @@ import { idStringifier } from '../utils/transformers'; import { datetimeColumnType } from './AbstractEntity'; import { WorkflowEntity } from './WorkflowEntity'; -export enum StatisticsNames { +export const enum StatisticsNames { productionSuccess = 'production_success', productionError = 'production_error', manualSuccess = 'manual_success', diff --git a/packages/cli/src/events/WorkflowStatistics.ts b/packages/cli/src/events/WorkflowStatistics.ts index 5567ded3b8afd..863aeb6e1897c 100644 --- a/packages/cli/src/events/WorkflowStatistics.ts +++ b/packages/cli/src/events/WorkflowStatistics.ts @@ -9,7 +9,7 @@ import { InternalHooks } from '@/InternalHooks'; import config from '@/config'; import { UserService } from '@/user/user.service'; -enum StatisticsUpsertResult { +const enum StatisticsUpsertResult { insert = 'insert', update = 'update', failed = 'failed', diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 00c80ca47fa8f..84b072f545083 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -635,7 +635,7 @@ export interface IN8nPromptResponse { updated: boolean; } -export enum UserManagementAuthenticationMethod { +export const enum UserManagementAuthenticationMethod { Email = 'email', Ldap = 'ldap', Saml = 'saml', diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index f600da467ac3a..c4b0914103a5d 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -362,7 +362,7 @@ export const NODE_TYPE_COUNT_MAPPER = { }; export const TEMPLATES_NODES_FILTER = ['n8n-nodes-base.start', 'n8n-nodes-base.respondToWebhook']; -export enum VIEWS { +export const enum VIEWS { HOMEPAGE = 'Homepage', COLLECTION = 'TemplatesCollectionView', EXECUTIONS = 'Executions', @@ -398,7 +398,7 @@ export enum VIEWS { VERSION_CONTROL = 'VersionControl', } -export enum FAKE_DOOR_FEATURES { +export const enum FAKE_DOOR_FEATURES { ENVIRONMENTS = 'environments', LOGGING = 'logging', SSO = 'sso', @@ -443,7 +443,7 @@ export const MAPPING_PARAMS = [ export const DEFAULT_STICKY_HEIGHT = 160; export const DEFAULT_STICKY_WIDTH = 240; -export enum WORKFLOW_MENU_ACTIONS { +export const enum WORKFLOW_MENU_ACTIONS { DUPLICATE = 'duplicate', DOWNLOAD = 'download', IMPORT_FROM_URL = 'import-from-url', @@ -455,7 +455,7 @@ export enum WORKFLOW_MENU_ACTIONS { /** * Enterprise edition */ -export enum EnterpriseEditionFeature { +export const enum EnterpriseEditionFeature { AdvancedExecutionFilters = 'advancedExecutionFilters', Sharing = 'sharing', Ldap = 'ldap', @@ -466,7 +466,7 @@ export enum EnterpriseEditionFeature { } export const MAIN_NODE_PANEL_WIDTH = 360; -export enum MAIN_HEADER_TABS { +export const enum MAIN_HEADER_TABS { WORKFLOW = 'workflow', EXECUTIONS = 'executions', SETTINGS = 'settings', @@ -504,7 +504,7 @@ export const CURL_IMPORT_NODES_PROTOCOLS: { [key: string]: string } = { imaps: 'IMAP', }; -export enum STORES { +export const enum STORES { COMMUNITY_NODES = 'communityNodes', ROOT = 'root', SETTINGS = 'settings', @@ -523,7 +523,7 @@ export enum STORES { HISTORY = 'history', } -export enum SignInType { +export const enum SignInType { LDAP = 'ldap', EMAIL = 'email', } diff --git a/packages/editor-ui/src/models/history.ts b/packages/editor-ui/src/models/history.ts index 5a404b27f3f2b..72733ab5ed155 100644 --- a/packages/editor-ui/src/models/history.ts +++ b/packages/editor-ui/src/models/history.ts @@ -6,7 +6,7 @@ import { createEventBus } from '@/event-bus'; // Command names don't serve any particular purpose in the app // but they make it easier to identify each command on stack // when debugging -export enum COMMANDS { +export const enum COMMANDS { MOVE_NODE = 'moveNode', ADD_NODE = 'addNode', REMOVE_NODE = 'removeNode', diff --git a/packages/editor-ui/src/permissions.ts b/packages/editor-ui/src/permissions.ts index a2300d6399b7f..0635491cc9943 100644 --- a/packages/editor-ui/src/permissions.ts +++ b/packages/editor-ui/src/permissions.ts @@ -14,7 +14,7 @@ import { import { EnterpriseEditionFeature, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants'; import { useSettingsStore } from './stores/settings'; -export enum UserRole { +export const enum UserRole { InstanceOwner = 'isInstanceOwner', ResourceOwner = 'isOwner', ResourceEditor = 'isEditor', diff --git a/packages/nodes-base/.eslintrc.js b/packages/nodes-base/.eslintrc.js index a5d9b31cce7a0..a06183204c694 100644 --- a/packages/nodes-base/.eslintrc.js +++ b/packages/nodes-base/.eslintrc.js @@ -24,7 +24,6 @@ module.exports = { '@typescript-eslint/naming-convention': ['error', { selector: 'memberLike', format: null }], '@typescript-eslint/no-explicit-any': 'off', //812 warnings, better to fix in separate PR '@typescript-eslint/no-non-null-assertion': 'off', //665 errors, better to fix in separate PR - // '@typescript-eslint/no-unsafe-argument': 'off', //1538 errors, better to fix in separate PR '@typescript-eslint/no-unsafe-assignment': 'off', //7084 problems, better to fix in separate PR '@typescript-eslint/no-unsafe-call': 'off', //541 errors, better to fix in separate PR '@typescript-eslint/no-unsafe-member-access': 'off', //4591 errors, better to fix in separate PR diff --git a/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts b/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts index c03eb89ab59a1..201fecbc34ee3 100644 --- a/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts +++ b/packages/nodes-base/nodes/Aws/DynamoDB/types.d.ts @@ -51,7 +51,7 @@ export type PartitionKey = { }; }; -export enum EAttributeValueType { +export const enum EAttributeValueType { S = 'S', SS = 'SS', M = 'M', diff --git a/packages/nodes-base/nodes/Clockify/CommonDtos.ts b/packages/nodes-base/nodes/Clockify/CommonDtos.ts index 858b58bb97b5c..7e7f5bbd91419 100644 --- a/packages/nodes-base/nodes/Clockify/CommonDtos.ts +++ b/packages/nodes-base/nodes/Clockify/CommonDtos.ts @@ -3,14 +3,14 @@ export interface IHourlyRateDto { currency: string; } -enum MembershipStatusEnum { +const enum MembershipStatusEnum { PENDING = 'PENDING', ACTIVE = 'ACTIVE', DECLINED = 'DECLINED', INACTIVE = 'INACTIVE', } -enum TaskStatusEnum { +const enum TaskStatusEnum { ACTIVE = 'ACTIVE', DONE = 'DONE', } diff --git a/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts index 0df2a710191ae..d83881685f8ed 100644 --- a/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts +++ b/packages/nodes-base/nodes/Clockify/EntryTypeEnum.ts @@ -1,3 +1,3 @@ -export enum EntryTypeEnum { +export const enum EntryTypeEnum { NEW_TIME_ENTRY, } diff --git a/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts b/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts index 0e7419cb69927..623a9fe5176fe 100644 --- a/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts +++ b/packages/nodes-base/nodes/Clockify/ProjectInterfaces.ts @@ -1,6 +1,6 @@ import type { IHourlyRateDto, IMembershipDto } from './CommonDtos'; -enum EstimateEnum { +const enum EstimateEnum { AUTO = 'AUTO', MANUAL = 'MANUAL', } @@ -40,7 +40,7 @@ export interface IProjectRequest { tasks: ITaskDto; } -enum TaskStatusEnum { +const enum TaskStatusEnum { ACTIVE = 'ACTIVE', DONE = 'DONE', } diff --git a/packages/nodes-base/nodes/Clockify/UserDtos.ts b/packages/nodes-base/nodes/Clockify/UserDtos.ts index dc1f96c75bf2e..6365b057dfaeb 100644 --- a/packages/nodes-base/nodes/Clockify/UserDtos.ts +++ b/packages/nodes-base/nodes/Clockify/UserDtos.ts @@ -1,7 +1,7 @@ import type { IDataObject } from 'n8n-workflow'; import type { IMembershipDto } from './CommonDtos'; -enum UserStatusEnum { +const enum UserStatusEnum { ACTIVE, PENDING_EMAIL_VERIFICATION, DELETED, diff --git a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts index f46be67faae2f..11eb0557e7dba 100644 --- a/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts +++ b/packages/nodes-base/nodes/Clockify/WorkpaceInterfaces.ts @@ -1,12 +1,12 @@ import type { IHourlyRateDto, IMembershipDto } from './CommonDtos'; -enum AdminOnlyPagesEnum { +const enum AdminOnlyPagesEnum { PROJECT = 'PROJECT', TEAM = 'TEAM', REPORTS = 'REPORTS', } -enum DaysOfWeekEnum { +const enum DaysOfWeekEnum { MONDAY = 'MONDAY', TUESDAY = 'TUESDAY', WEDNESDAY = 'WEDNESDAY', @@ -16,13 +16,13 @@ enum DaysOfWeekEnum { SUNDAY = 'SUNDAY', } -enum DatePeriodEnum { +const enum DatePeriodEnum { DAYS = 'DAYS', WEEKS = 'WEEKS', MONTHS = 'MONTHS', } -enum AutomaticLockTypeEnum { +const enum AutomaticLockTypeEnum { WEEKLY = 'WEEKLY', MONTHLY = 'MONTHLY', OLDER_THAN = 'OLDER_THAN', diff --git a/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts b/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts index 09e3a6ff57fc2..5f9f8fe8274e1 100644 --- a/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts +++ b/packages/nodes-base/nodes/Cortex/AnalyzerInterface.ts @@ -1,6 +1,6 @@ import type { IDataObject } from 'n8n-workflow'; -export enum JobStatus { +export const enum JobStatus { WAITING = 'Waiting', INPROGRESS = 'InProgress', SUCCESS = 'Success', @@ -8,14 +8,14 @@ export enum JobStatus { DELETED = 'Deleted', } -export enum TLP { +export const enum TLP { white, green, amber, red, } -export enum ObservableDataType { +export const enum ObservableDataType { 'domain' = 'domain', 'file' = 'file', 'filename' = 'filename', diff --git a/packages/nodes-base/nodes/Formstack/GenericFunctions.ts b/packages/nodes-base/nodes/Formstack/GenericFunctions.ts index 7de8096dfc20e..de19384130ccc 100644 --- a/packages/nodes-base/nodes/Formstack/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Formstack/GenericFunctions.ts @@ -39,7 +39,7 @@ export interface IFormstackSubmissionFieldContainer { value: string; } -export enum FormstackFieldFormat { +export const enum FormstackFieldFormat { ID = 'id', Label = 'label', Name = 'name', diff --git a/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts b/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts index 428a8a7731cf6..dbeb8d14d64de 100644 --- a/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts +++ b/packages/nodes-base/nodes/Freshdesk/Freshdesk.node.ts @@ -20,21 +20,21 @@ import type { ICreateContactBody } from './ContactInterface'; import { contactFields, contactOperations } from './ContactDescription'; -enum Status { +const enum Status { Open = 2, Pending = 3, Resolved = 4, Closed = 5, } -enum Priority { +const enum Priority { Low = 1, Medium = 2, High = 3, Urgent = 4, } -enum Source { +const enum Source { Email = 1, Portal = 2, Phone = 3, diff --git a/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts index 8dc2d0e570185..a03ab9f1306a3 100644 --- a/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts +++ b/packages/nodes-base/nodes/Google/Chat/MessageInterface.ts @@ -31,7 +31,7 @@ export interface IUser { type?: Type; isAnonymous?: boolean; } -enum Type { +const enum Type { 'TYPE_UNSPECIFIED', 'HUMAN', 'BOT', diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts index aac00508c4595..a55fc71ae4dd7 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/helpers/GoogleSheets.types.ts @@ -64,11 +64,11 @@ export type SheetProperties = PropertiesOf; export type ResourceLocator = 'id' | 'url' | 'list'; -export enum ResourceLocatorUiNames { - id = 'By ID', - url = 'By URL', - list = 'From List', -} +export const ResourceLocatorUiNames = { + id: 'By ID', + url: 'By URL', + list: 'From List', +}; export type SheetCellDecoded = { cell?: string; diff --git a/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts b/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts index 0e0db54bcb7c0..81ee759a3589a 100644 --- a/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts +++ b/packages/nodes-base/nodes/ItemLists/V1/summarize.operation.ts @@ -29,16 +29,16 @@ type Aggregation = { type Aggregations = Aggregation[]; -enum AggregationDisplayNames { - append = 'appended_', - average = 'average_', - concatenate = 'concatenated_', - count = 'count_', - countUnique = 'unique_count_', - max = 'max_', - min = 'min_', - sum = 'sum_', -} +const AggregationDisplayNames = { + append: 'appended_', + average: 'average_', + concatenate: 'concatenated_', + count: 'count_', + countUnique: 'unique_count_', + max: 'max_', + min: 'min_', + sum: 'sum_', +}; const NUMERICAL_AGGREGATIONS = ['average', 'max', 'min', 'sum']; diff --git a/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts b/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts index c3accbd330659..72347b4cb44b6 100644 --- a/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts +++ b/packages/nodes-base/nodes/ItemLists/V2/summarize.operation.ts @@ -29,16 +29,17 @@ type Aggregation = { type Aggregations = Aggregation[]; -enum AggregationDisplayNames { - append = 'appended_', - average = 'average_', - concatenate = 'concatenated_', - count = 'count_', - countUnique = 'unique_count_', - max = 'max_', - min = 'min_', - sum = 'sum_', -} +// eslint-disable-next-line no-restricted-syntax +const AggregationDisplayNames = { + append: 'appended_', + average: 'average_', + concatenate: 'concatenated_', + count: 'count_', + countUnique: 'unique_count_', + max: 'max_', + min: 'min_', + sum: 'sum_', +}; const NUMERICAL_AGGREGATIONS = ['average', 'max', 'min', 'sum']; diff --git a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts index 8359700365176..0d3728ad2cdc5 100644 --- a/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts +++ b/packages/nodes-base/nodes/Mailchimp/Mailchimp.node.ts @@ -17,7 +17,7 @@ import { import moment from 'moment'; -enum Status { +const enum Status { subscribe = 'subscribe', unsubscribed = 'unsubscribe', cleaned = 'cleaned', diff --git a/packages/nodes-base/nodes/PayPal/PaymentInteface.ts b/packages/nodes-base/nodes/PayPal/PaymentInteface.ts index 53b2ec1457918..d4e28c6ceef7d 100644 --- a/packages/nodes-base/nodes/PayPal/PaymentInteface.ts +++ b/packages/nodes-base/nodes/PayPal/PaymentInteface.ts @@ -1,10 +1,10 @@ -export enum RecipientType { +export const enum RecipientType { email = 'EMAIL', phone = 'PHONE', paypalId = 'PAYPAL_ID', } -export enum RecipientWallet { +export const enum RecipientWallet { paypal = 'PAYPAL', venmo = 'VENMO', } diff --git a/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts b/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts index 73ab3272cf970..7fa7a6e4bdb74 100644 --- a/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts +++ b/packages/nodes-base/nodes/SendInBlue/GenericFunctions.ts @@ -18,13 +18,13 @@ export namespace SendInBlueNode { type BBCEmail = { bbc: Email[] }; type ValidatedEmail = ToEmail | SenderEmail | CCEmail | BBCEmail; - enum OVERRIDE_MAP_VALUES { + const enum OVERRIDE_MAP_VALUES { 'CATEGORY' = 'category', 'NORMAL' = 'boolean', 'TRANSACTIONAL' = 'id', } - enum OVERRIDE_MAP_TYPE { + const enum OVERRIDE_MAP_TYPE { 'CATEGORY' = 'category', 'NORMAL' = 'normal', 'TRANSACTIONAL' = 'transactional', diff --git a/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts index f8ee11271a102..0286d8247a88d 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/AlertInterface.ts @@ -1,11 +1,11 @@ import type { IDataObject } from 'n8n-workflow'; -export enum AlertStatus { +export const enum AlertStatus { NEW = 'New', UPDATED = 'Updated', IGNORED = 'Ignored', IMPORTED = 'Imported', } -export enum TLP { +export const enum TLP { white, green, amber, diff --git a/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts index 58e910c6ffd0a..297ae58f5185f 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/CaseInterface.ts @@ -31,13 +31,13 @@ export interface ICase { upadtedAt?: Date; } -export enum CaseStatus { +export const enum CaseStatus { OPEN = 'Open', RESOLVED = 'Resolved', DELETED = 'Deleted', } -export enum CaseResolutionStatus { +export const enum CaseResolutionStatus { INDETERMINATE = 'Indeterminate', FALSEPOSITIVE = 'FalsePositive', TRUEPOSITIVE = 'TruePositive', @@ -45,7 +45,7 @@ export enum CaseResolutionStatus { DUPLICATED = 'Duplicated', } -export enum CaseImpactStatus { +export const enum CaseImpactStatus { NOIMPACT = 'NoImpact', WITHIMPACT = 'WithImpact', NOTAPPLICABLE = 'NotApplicable', diff --git a/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts index 5e02a41c627f1..2e786a962f89a 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/LogInterface.ts @@ -1,5 +1,5 @@ import type { IAttachment } from './ObservableInterface'; -export enum LogStatus { +export const enum LogStatus { OK = 'Ok', DELETED = 'Deleted', } diff --git a/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts index a18b85c2db2fd..cb35df2189870 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/ObservableInterface.ts @@ -1,10 +1,10 @@ import type { TLP } from './AlertInterface'; -export enum ObservableStatus { +export const enum ObservableStatus { OK = 'Ok', DELETED = 'Deleted', } -export enum ObservableDataType { +export const enum ObservableDataType { 'domain' = 'domain', 'file' = 'file', 'filename' = 'filename', diff --git a/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts b/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts index c453e10b1ede4..daff422198e8b 100644 --- a/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts +++ b/packages/nodes-base/nodes/TheHive/interfaces/TaskInterface.ts @@ -17,7 +17,7 @@ export interface ITask { upadtedAt?: Date; } -export enum TaskStatus { +export const enum TaskStatus { WAITING = 'Waiting', INPROGRESS = 'InProgress', COMPLETED = 'Completed', diff --git a/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts b/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts index 8225bad42e402..a45773a8809c7 100644 --- a/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts +++ b/packages/nodes-base/nodes/Todoist/v1/OperationHandler.ts @@ -42,7 +42,7 @@ export interface Command { }; } -export enum CommandType { +export const enum CommandType { ITEM_MOVE = 'item_move', ITEM_ADD = 'item_add', ITEM_UPDATE = 'item_update', diff --git a/packages/nodes-base/nodes/Todoist/v1/Service.ts b/packages/nodes-base/nodes/Todoist/v1/Service.ts index 3e951608a89c3..e40fe25be02e4 100644 --- a/packages/nodes-base/nodes/Todoist/v1/Service.ts +++ b/packages/nodes-base/nodes/Todoist/v1/Service.ts @@ -35,17 +35,16 @@ export class TodoistService implements Service { }; } -export enum OperationType { - create = 'create', - close = 'close', - delete = 'delete', - get = 'get', - getAll = 'getAll', - reopen = 'reopen', - update = 'update', - move = 'move', - sync = 'sync', -} +export type OperationType = + | 'create' + | 'close' + | 'delete' + | 'get' + | 'getAll' + | 'reopen' + | 'update' + | 'move' + | 'sync'; export interface Section { name: string; diff --git a/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts b/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts index 6dcecc9787644..adf48942cfe3b 100644 --- a/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts +++ b/packages/nodes-base/nodes/Todoist/v1/TodoistV1.node.ts @@ -13,7 +13,8 @@ import type { import { todoistApiRequest } from '../GenericFunctions'; -import { OperationType, TodoistService } from './Service'; +import type { OperationType } from './Service'; +import { TodoistService } from './Service'; // interface IBodyCreateTask { // content?: string; @@ -702,15 +703,11 @@ export class TodoistV1 implements INodeType { const service = new TodoistService(); let responseData; const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); + const operation = this.getNodeParameter('operation', 0) as OperationType; for (let i = 0; i < length; i++) { try { if (resource === 'task') { - responseData = await service.execute( - this, - OperationType[operation as keyof typeof OperationType], - i, - ); + responseData = await service.execute(this, operation, i); } if (Array.isArray(responseData?.data)) { returnData.push.apply(returnData, responseData?.data as IDataObject[]); diff --git a/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts b/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts index 179d958be7ec0..fbe65589c8343 100644 --- a/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts +++ b/packages/nodes-base/nodes/Todoist/v2/OperationHandler.ts @@ -42,7 +42,7 @@ export interface Command { }; } -export enum CommandType { +export const enum CommandType { ITEM_MOVE = 'item_move', ITEM_ADD = 'item_add', ITEM_UPDATE = 'item_update', diff --git a/packages/nodes-base/nodes/Todoist/v2/Service.ts b/packages/nodes-base/nodes/Todoist/v2/Service.ts index 3e951608a89c3..e40fe25be02e4 100644 --- a/packages/nodes-base/nodes/Todoist/v2/Service.ts +++ b/packages/nodes-base/nodes/Todoist/v2/Service.ts @@ -35,17 +35,16 @@ export class TodoistService implements Service { }; } -export enum OperationType { - create = 'create', - close = 'close', - delete = 'delete', - get = 'get', - getAll = 'getAll', - reopen = 'reopen', - update = 'update', - move = 'move', - sync = 'sync', -} +export type OperationType = + | 'create' + | 'close' + | 'delete' + | 'get' + | 'getAll' + | 'reopen' + | 'update' + | 'move' + | 'sync'; export interface Section { name: string; diff --git a/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts b/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts index 6cb751d787161..f3813ca59be54 100644 --- a/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts +++ b/packages/nodes-base/nodes/Todoist/v2/TodoistV2.node.ts @@ -13,7 +13,8 @@ import type { import { todoistApiRequest } from '../GenericFunctions'; -import { OperationType, TodoistService } from './Service'; +import type { OperationType } from './Service'; +import { TodoistService } from './Service'; // interface IBodyCreateTask { // content?: string; @@ -701,15 +702,11 @@ export class TodoistV2 implements INodeType { const service = new TodoistService(); let responseData; const resource = this.getNodeParameter('resource', 0); - const operation = this.getNodeParameter('operation', 0); + const operation = this.getNodeParameter('operation', 0) as OperationType; for (let i = 0; i < length; i++) { try { if (resource === 'task') { - responseData = await service.execute( - this, - OperationType[operation as keyof typeof OperationType], - i, - ); + responseData = await service.execute(this, operation, i); } if (responseData !== undefined && Array.isArray(responseData?.data)) { diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 6c1b687b3f792..e3eccd8a0dbc2 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1868,7 +1868,7 @@ export interface IConnectedNode { depth: number; } -export enum OAuth2GrantType { +export const enum OAuth2GrantType { authorizationCode = 'authorizationCode', clientCredentials = 'clientCredentials', } diff --git a/packages/workflow/src/MessageEventBus.ts b/packages/workflow/src/MessageEventBus.ts index cf604ea6555c2..2da8c7a20d826 100644 --- a/packages/workflow/src/MessageEventBus.ts +++ b/packages/workflow/src/MessageEventBus.ts @@ -5,7 +5,7 @@ import type { INodeCredentials } from './Interfaces'; // General Enums And Interfaces // =============================== -export enum EventMessageTypeNames { +export const enum EventMessageTypeNames { generic = '$$EventMessage', audit = '$$EventMessageAudit', confirm = '$$EventMessageConfirm', @@ -13,7 +13,7 @@ export enum EventMessageTypeNames { node = '$$EventMessageNode', } -export enum MessageEventBusDestinationTypeNames { +export const enum MessageEventBusDestinationTypeNames { abstract = '$$AbstractMessageEventBusDestination', webhook = '$$MessageEventBusDestinationWebhook', sentry = '$$MessageEventBusDestinationSentry', From ac245fdb8d99fce1ab32020f2e18653f04b9ca76 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Fri, 21 Apr 2023 13:30:57 +0200 Subject: [PATCH 005/110] refactor(editor): Consolidate IN8nUISettings interface (#6055) * consolidate IN8nUISettings * cleanup --- packages/cli/src/Interfaces.ts | 101 ---------------- packages/cli/src/Server.ts | 6 +- packages/editor-ui/src/Interface.ts | 100 +--------------- .../__tests__/server/endpoints/settings.ts | 22 +++- packages/editor-ui/src/api/settings.ts | 2 +- .../src/components/WorkflowSettings.vue | 4 +- .../src/stores/__tests__/posthog.test.ts | 2 +- .../src/stores/__tests__/sso.test.ts | 2 +- packages/editor-ui/src/stores/settings.ts | 13 ++- .../src/utils/__tests__/userUtils.test.ts | 3 +- packages/workflow/src/Interfaces.ts | 110 +++++++++++++++++- 11 files changed, 151 insertions(+), 214 deletions(-) diff --git a/packages/cli/src/Interfaces.ts b/packages/cli/src/Interfaces.ts index ddbfce8abfa84..b6f1dd984ac1d 100644 --- a/packages/cli/src/Interfaces.ts +++ b/packages/cli/src/Interfaces.ts @@ -13,7 +13,6 @@ import type { IRunData, IRunExecutionData, ITaskData, - ITelemetrySettings, ITelemetryTrackProperties, IWorkflowBase, CredentialLoadingDetails, @@ -23,8 +22,6 @@ import type { ExecutionStatus, IExecutionsSummary, FeatureFlags, - WorkflowSettings, - AuthenticationMethod, } from 'n8n-workflow'; import type { ActiveWorkflowRunner } from '@/ActiveWorkflowRunner'; @@ -469,90 +466,6 @@ export interface IVersionNotificationSettings { infoUrl: string; } -export interface IN8nUISettings { - endpointWebhook: string; - endpointWebhookTest: string; - saveDataErrorExecution: WorkflowSettings.SaveDataExecution; - saveDataSuccessExecution: WorkflowSettings.SaveDataExecution; - saveManualExecutions: boolean; - executionTimeout: number; - maxExecutionTimeout: number; - workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy; - oauthCallbackUrls: { - oauth1: string; - oauth2: string; - }; - timezone: string; - urlBaseWebhook: string; - urlBaseEditor: string; - versionCli: string; - n8nMetadata?: { - [key: string]: string | number | undefined; - }; - versionNotifications: IVersionNotificationSettings; - instanceId: string; - telemetry: ITelemetrySettings; - posthog: { - enabled: boolean; - apiHost: string; - apiKey: string; - autocapture: boolean; - disableSessionRecording: boolean; - debug: boolean; - }; - personalizationSurveyEnabled: boolean; - userActivationSurveyEnabled: boolean; - defaultLocale: string; - userManagement: IUserManagementSettings; - sso: { - saml: { - loginLabel: string; - loginEnabled: boolean; - }; - ldap: { - loginLabel: string; - loginEnabled: boolean; - }; - }; - publicApi: IPublicApiSettings; - workflowTagsDisabled: boolean; - logLevel: 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent'; - hiringBannerEnabled: boolean; - templates: { - enabled: boolean; - host: string; - }; - onboardingCallPromptEnabled: boolean; - missingPackages?: boolean; - executionMode: 'regular' | 'queue'; - pushBackend: 'sse' | 'websocket'; - communityNodesEnabled: boolean; - deployment: { - type: string; - }; - isNpmAvailable: boolean; - allowedModules: { - builtIn?: string; - external?: string; - }; - enterprise: { - sharing: boolean; - ldap: boolean; - saml: boolean; - logStreaming: boolean; - advancedExecutionFilters: boolean; - variables: boolean; - versionControl: boolean; - }; - hideUsagePage: boolean; - license: { - environment: 'production' | 'staging'; - }; - variables: { - limit: number; - }; -} - export interface IPersonalizationSurveyAnswers { email: string | null; codingSkill: string | null; @@ -570,23 +483,9 @@ export interface IUserSettings { userActivated?: boolean; } -export interface IUserManagementSettings { - enabled: boolean; - showSetupOnFirstLoad?: boolean; - smtpSetup: boolean; - authenticationMethod: AuthenticationMethod; -} export interface IActiveDirectorySettings { enabled: boolean; } -export interface IPublicApiSettings { - enabled: boolean; - latestVersion: number; - path: string; - swaggerUi: { - enabled: boolean; - }; -} export interface IPackageVersions { cli: string; diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 49320cfbc2537..c6ae91e2800c2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -49,6 +49,7 @@ import type { ICredentialTypes, ExecutionStatus, IExecutionsSummary, + IN8nUISettings, } from 'n8n-workflow'; import { LoggerProxy, jsonParse } from 'n8n-workflow'; @@ -112,7 +113,6 @@ import type { IDiagnosticInfo, IExecutionFlattedDb, IExecutionsStopData, - IN8nUISettings, } from '@/Interfaces'; import { ActiveExecutions } from '@/ActiveExecutions'; import { @@ -313,8 +313,8 @@ class Server extends AbstractServer { }, isNpmAvailable: false, allowedModules: { - builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN, - external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL, + builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN?.split(',') ?? undefined, + external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL?.split(',') ?? undefined, }, enterprise: { sharing: false, diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index 84b072f545083..9edcf0e76ab69 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -17,7 +17,6 @@ import { IRun, IRunData, ITaskData, - ITelemetrySettings, IWorkflowSettings as IWorkflowSettingsWorkflow, WorkflowExecuteMode, PublicInstalledPackage, @@ -33,6 +32,9 @@ import { FeatureFlags, ExecutionStatus, ITelemetryTrackProperties, + IN8nUISettings, + IUserManagementSettings, + WorkflowSettings, } from 'n8n-workflow'; import { SignInType } from './constants'; import { FAKE_DOOR_FEATURES, TRIGGER_NODE_FILTER, REGULAR_NODE_FILTER } from './constants'; @@ -641,13 +643,6 @@ export const enum UserManagementAuthenticationMethod { Saml = 'saml', } -export interface IUserManagementConfig { - enabled: boolean; - showSetupOnFirstLoad?: boolean; - smtpSetup: boolean; - authenticationMethod: UserManagementAuthenticationMethod; -} - export interface IPermissionGroup { loginStatus?: ILogInStatus[]; role?: IRole[]; @@ -732,94 +727,14 @@ export interface ITemplatesCategory { export type WorkflowCallerPolicyDefaultOption = 'any' | 'none' | 'workflowsFromAList'; -export interface IN8nUISettings { - endpointWebhook: string; - endpointWebhookTest: string; - saveDataErrorExecution: string; - saveDataSuccessExecution: string; - saveManualExecutions: boolean; - workflowCallerPolicyDefaultOption: WorkflowCallerPolicyDefaultOption; - timezone: string; - executionTimeout: number; - maxExecutionTimeout: number; - oauthCallbackUrls: { - oauth1: string; - oauth2: string; - }; - urlBaseEditor: string; - urlBaseWebhook: string; - versionCli: string; - n8nMetadata?: { - [key: string]: string | number | undefined; - }; - versionNotifications: IVersionNotificationSettings; - instanceId: string; - personalizationSurveyEnabled: boolean; - userActivationSurveyEnabled: boolean; - telemetry: ITelemetrySettings; - userManagement: IUserManagementConfig; - defaultLocale: string; - workflowTagsDisabled: boolean; - logLevel: ILogLevel; - hiringBannerEnabled: boolean; - templates: { - enabled: boolean; - host: string; - }; - posthog: { - enabled: boolean; - apiHost: string; - apiKey: string; - autocapture: boolean; - disableSessionRecording: boolean; - debug: boolean; - }; - executionMode: string; - pushBackend: 'sse' | 'websocket'; - communityNodesEnabled: boolean; - isNpmAvailable: boolean; - publicApi: { - enabled: boolean; - latestVersion: number; - path: string; - swaggerUi: { - enabled: boolean; - }; - }; - sso: { - saml: { - loginLabel: string; - loginEnabled: boolean; - }; - ldap: { - loginLabel: string; - loginEnabled: boolean; - }; - }; - onboardingCallPromptEnabled: boolean; - allowedModules: { - builtIn?: string[]; - external?: string[]; - }; - enterprise: Record; - deployment?: { - type: string | 'default' | 'n8n-internal' | 'cloud' | 'desktop_mac' | 'desktop_win'; - }; - hideUsagePage: boolean; - license: { - environment: 'development' | 'production'; - }; -} - export interface IWorkflowSettings extends IWorkflowSettingsWorkflow { errorWorkflow?: string; - saveDataErrorExecution?: string; - saveDataSuccessExecution?: string; saveManualExecutions?: boolean; timezone?: string; executionTimeout?: number; + maxExecutionTimeout?: number; callerIds?: string; - callerPolicy?: WorkflowCallerPolicyDefaultOption; + callerPolicy?: WorkflowSettings.CallerPolicy; } export interface ITimeoutHMS { @@ -1177,9 +1092,6 @@ export interface UIState { addFirstStepOnLoad: boolean; executionSidebarAutoRefresh: boolean; } - -export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose'; - export type IFakeDoor = { id: FAKE_DOOR_FEATURES; featureName: string; @@ -1221,7 +1133,7 @@ export interface INodeCreatorState { export interface ISettingsState { settings: IN8nUISettings; promptsData: IN8nPrompts; - userManagement: IUserManagementConfig; + userManagement: IUserManagementSettings; templatesEndpointHealthy: boolean; api: { enabled: boolean; diff --git a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts index 374e548d8841c..e2b5b4e0eb7e7 100644 --- a/packages/editor-ui/src/__tests__/server/endpoints/settings.ts +++ b/packages/editor-ui/src/__tests__/server/endpoints/settings.ts @@ -1,6 +1,6 @@ import { Response, Server } from 'miragejs'; import { AppSchema } from '../types'; -import { IN8nUISettings, ISettingsState } from '@/Interface'; +import { IN8nUISettings } from 'n8n-workflow'; const defaultSettings: IN8nUISettings = { allowedModules: {}, @@ -9,9 +9,15 @@ const defaultSettings: IN8nUISettings = { endpointWebhook: '', endpointWebhookTest: '', enterprise: { + sharing: false, + ldap: false, + saml: false, + logStreaming: false, + advancedExecutionFilters: false, variables: true, + versionControl: false, }, - executionMode: '', + executionMode: 'regular', executionTimeout: 0, hideUsagePage: false, hiringBannerEnabled: false, @@ -33,8 +39,8 @@ const defaultSettings: IN8nUISettings = { }, publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } }, pushBackend: 'websocket', - saveDataErrorExecution: '', - saveDataSuccessExecution: '', + saveDataErrorExecution: 'DEFAULT', + saveDataSuccessExecution: 'DEFAULT', saveManualExecutions: false, sso: { ldap: { loginEnabled: false, loginLabel: '' }, @@ -51,6 +57,7 @@ const defaultSettings: IN8nUISettings = { enabled: true, showSetupOnFirstLoad: true, smtpSetup: true, + authenticationMethod: 'email', }, versionCli: '', versionNotifications: { @@ -60,6 +67,13 @@ const defaultSettings: IN8nUISettings = { }, workflowCallerPolicyDefaultOption: 'any', workflowTagsDisabled: false, + variables: { + limit: -1, + }, + userActivationSurveyEnabled: false, + deployment: { + type: 'default', + }, }; export function routesForSettings(server: Server) { diff --git a/packages/editor-ui/src/api/settings.ts b/packages/editor-ui/src/api/settings.ts index 9bd87f28f7259..2205994b7eace 100644 --- a/packages/editor-ui/src/api/settings.ts +++ b/packages/editor-ui/src/api/settings.ts @@ -2,11 +2,11 @@ import { IRestApiContext, IN8nPrompts, IN8nValueSurveyData, - IN8nUISettings, IN8nPromptResponse, } from '../Interface'; import { makeRestApiRequest, get, post } from '@/utils'; import { N8N_IO_BASE_URL, NPM_COMMUNITY_NODE_SEARCH_API_URL } from '@/constants'; +import { IN8nUISettings } from 'n8n-workflow'; export function getSettings(context: IRestApiContext): Promise { return makeRestApiRequest(context, 'GET', '/settings'); diff --git a/packages/editor-ui/src/components/WorkflowSettings.vue b/packages/editor-ui/src/components/WorkflowSettings.vue index 04a4c0f0ae097..44155cd3dc3eb 100644 --- a/packages/editor-ui/src/components/WorkflowSettings.vue +++ b/packages/editor-ui/src/components/WorkflowSettings.vue @@ -348,7 +348,7 @@ import { import mixins from 'vue-typed-mixins'; -import { deepCopy } from 'n8n-workflow'; +import { WorkflowSettings, deepCopy } from 'n8n-workflow'; import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; import { useSettingsStore } from '@/stores/settings'; @@ -504,7 +504,7 @@ export default mixins(externalHooks, genericHelpers, restApi, showMessage).exten } if (workflowSettings.callerPolicy === undefined) { workflowSettings.callerPolicy = this.defaultValues - .workflowCallerPolicy as WorkflowCallerPolicyDefaultOption; + .workflowCallerPolicy as WorkflowSettings.CallerPolicy; } if (workflowSettings.executionTimeout === undefined) { workflowSettings.executionTimeout = this.rootStore.executionTimeout; diff --git a/packages/editor-ui/src/stores/__tests__/posthog.test.ts b/packages/editor-ui/src/stores/__tests__/posthog.test.ts index ed31f1a252e39..dbc870fc09598 100644 --- a/packages/editor-ui/src/stores/__tests__/posthog.test.ts +++ b/packages/editor-ui/src/stores/__tests__/posthog.test.ts @@ -2,9 +2,9 @@ import { createPinia, setActivePinia } from 'pinia'; import { usePostHog } from '@/stores/posthog'; import { useUsersStore } from '@/stores/users'; import { useSettingsStore } from '@/stores/settings'; -import { IN8nUISettings } from '@/Interface'; import { useRootStore } from '@/stores/n8nRootStore'; import { useTelemetryStore } from '@/stores/telemetry'; +import { IN8nUISettings } from 'n8n-workflow'; const DEFAULT_POSTHOG_SETTINGS: IN8nUISettings['posthog'] = { enabled: true, diff --git a/packages/editor-ui/src/stores/__tests__/sso.test.ts b/packages/editor-ui/src/stores/__tests__/sso.test.ts index eea1d3684e6e8..1e2f0b437b2c2 100644 --- a/packages/editor-ui/src/stores/__tests__/sso.test.ts +++ b/packages/editor-ui/src/stores/__tests__/sso.test.ts @@ -2,8 +2,8 @@ import { createPinia, setActivePinia } from 'pinia'; import { useSettingsStore } from '@/stores/settings'; import { useSSOStore } from '@/stores/sso'; import { merge } from 'lodash-es'; -import { IN8nUISettings } from '@/Interface'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { IN8nUISettings } from 'n8n-workflow'; let ssoStore: ReturnType; let settingsStore: ReturnType; diff --git a/packages/editor-ui/src/stores/settings.ts b/packages/editor-ui/src/stores/settings.ts index 24e48f5a1fbae..049ea61b59b5d 100644 --- a/packages/editor-ui/src/stores/settings.ts +++ b/packages/editor-ui/src/stores/settings.ts @@ -16,16 +16,19 @@ import { } from '@/constants'; import { ILdapConfig, - ILogLevel, IN8nPromptResponse, IN8nPrompts, - IN8nUISettings, IN8nValueSurveyData, ISettingsState, UserManagementAuthenticationMethod, - WorkflowCallerPolicyDefaultOption, } from '@/Interface'; -import { IDataObject, ITelemetrySettings } from 'n8n-workflow'; +import { + IDataObject, + ILogLevel, + IN8nUISettings, + ITelemetrySettings, + WorkflowSettings, +} from 'n8n-workflow'; import { defineStore } from 'pinia'; import Vue from 'vue'; import { useRootStore } from './n8nRootStore'; @@ -175,7 +178,7 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { isQueueModeEnabled(): boolean { return this.settings.executionMode === 'queue'; }, - workflowCallerPolicyDefaultOption(): WorkflowCallerPolicyDefaultOption { + workflowCallerPolicyDefaultOption(): WorkflowSettings.CallerPolicy { return this.settings.workflowCallerPolicyDefaultOption; }, isDefaultAuthenticationSaml(): boolean { diff --git a/packages/editor-ui/src/utils/__tests__/userUtils.test.ts b/packages/editor-ui/src/utils/__tests__/userUtils.test.ts index 0a9f98e64d9f3..fbebefc4b45d5 100644 --- a/packages/editor-ui/src/utils/__tests__/userUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/userUtils.test.ts @@ -4,10 +4,11 @@ import { merge } from 'lodash-es'; import { isAuthorized } from '@/utils'; import { useSettingsStore } from '@/stores/settings'; import { useSSOStore } from '@/stores/sso'; -import { IN8nUISettings, IUser } from '@/Interface'; +import { IUser } from '@/Interface'; import { routes } from '@/router'; import { VIEWS } from '@/constants'; import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils'; +import { IN8nUISettings } from 'n8n-workflow'; const DEFAULT_SETTINGS: IN8nUISettings = SETTINGS_STORE_DEFAULT_STATE.settings; diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index e3eccd8a0dbc2..bfc40f0ac87c3 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -17,6 +17,7 @@ import type { NodeApiError, NodeOperationError } from './NodeErrors'; import type { ExpressionError } from './ExpressionError'; import type { PathLike } from 'fs'; import type { ExecutionStatus } from './ExecutionStatus'; +import type { AuthenticationMethod } from './Authentication'; export interface IAdditionalCredentialOptions { oauth2?: IOAuth2Options; @@ -385,7 +386,6 @@ export interface IDataObject { [key: string]: GenericValue | IDataObject | GenericValue[] | IDataObject[]; } -// export type IExecuteResponsePromiseData = IDataObject; export type IExecuteResponsePromiseData = IDataObject | IN8nHttpFullResponse; export interface INodeTypeNameVersion { @@ -1948,3 +1948,111 @@ export interface ExecutionFilters { waitTill?: boolean; workflowId?: number | string; } + +export interface IVersionNotificationSettings { + enabled: boolean; + endpoint: string; + infoUrl: string; +} + +export interface IUserManagementSettings { + enabled: boolean; + showSetupOnFirstLoad?: boolean; + smtpSetup: boolean; + authenticationMethod: AuthenticationMethod; +} + +export interface IPublicApiSettings { + enabled: boolean; + latestVersion: number; + path: string; + swaggerUi: { + enabled: boolean; + }; +} + +export type ILogLevel = 'info' | 'debug' | 'warn' | 'error' | 'verbose' | 'silent'; + +export interface IN8nUISettings { + endpointWebhook: string; + endpointWebhookTest: string; + saveDataErrorExecution: WorkflowSettings.SaveDataExecution; + saveDataSuccessExecution: WorkflowSettings.SaveDataExecution; + saveManualExecutions: boolean; + executionTimeout: number; + maxExecutionTimeout: number; + workflowCallerPolicyDefaultOption: WorkflowSettings.CallerPolicy; + oauthCallbackUrls: { + oauth1: string; + oauth2: string; + }; + timezone: string; + urlBaseWebhook: string; + urlBaseEditor: string; + versionCli: string; + n8nMetadata?: { + [key: string]: string | number | undefined; + }; + versionNotifications: IVersionNotificationSettings; + instanceId: string; + telemetry: ITelemetrySettings; + posthog: { + enabled: boolean; + apiHost: string; + apiKey: string; + autocapture: boolean; + disableSessionRecording: boolean; + debug: boolean; + }; + personalizationSurveyEnabled: boolean; + userActivationSurveyEnabled: boolean; + defaultLocale: string; + userManagement: IUserManagementSettings; + sso: { + saml: { + loginLabel: string; + loginEnabled: boolean; + }; + ldap: { + loginLabel: string; + loginEnabled: boolean; + }; + }; + publicApi: IPublicApiSettings; + workflowTagsDisabled: boolean; + logLevel: ILogLevel; + hiringBannerEnabled: boolean; + templates: { + enabled: boolean; + host: string; + }; + onboardingCallPromptEnabled: boolean; + missingPackages?: boolean; + executionMode: 'regular' | 'queue'; + pushBackend: 'sse' | 'websocket'; + communityNodesEnabled: boolean; + deployment: { + type: string | 'default' | 'n8n-internal' | 'cloud' | 'desktop_mac' | 'desktop_win'; + }; + isNpmAvailable: boolean; + allowedModules: { + builtIn?: string[]; + external?: string[]; + }; + enterprise: { + sharing: boolean; + ldap: boolean; + saml: boolean; + logStreaming: boolean; + advancedExecutionFilters: boolean; + variables: boolean; + versionControl: boolean; + }; + hideUsagePage: boolean; + license: { + environment: 'development' | 'production' | 'staging'; + }; + variables: { + limit: number; + }; +} From d17d050a1660e8f808767271ef06e787ed615bf6 Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Fri, 21 Apr 2023 13:31:39 +0200 Subject: [PATCH 006/110] ci(editor): Do not run parallel jobs for a single spec (no-changelog) (#6052) * ci(editor): Do not run parallel jobs for a single spec * Fix syntax * Only post e2e success comment on actual e2e success * Set e2e-reusable output and check all container state --- .github/workflows/e2e-reusable.yml | 32 +++++++++++++++++++++++++++--- .github/workflows/e2e-tests-pr.yml | 2 +- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/e2e-reusable.yml b/.github/workflows/e2e-reusable.yml index c982d84e1fd1b..df2b04a058d74 100644 --- a/.github/workflows/e2e-reusable.yml +++ b/.github/workflows/e2e-reusable.yml @@ -47,6 +47,11 @@ on: CYPRESS_RECORD_KEY: description: 'Cypress record key.' required: true + outputs: + tests_passed: + description: 'True if all E2E tests passed, otherwise false' + value: ${{ jobs.check_testing_matrix.outputs.all_tests_passed }} + jobs: # single job that generates and outputs a common id @@ -109,7 +114,9 @@ jobs: strategy: fail-fast: false matrix: - containers: ${{ fromJSON(inputs.containers) }} + # If spec is not e2e/* then we run only one container to prevent + # running the same tests multiple times + containers: ${{ fromJSON( inputs.spec == 'e2e/*' && inputs.containers || '[1]' ) }} steps: - uses: actions/checkout@v3 with: @@ -135,9 +142,9 @@ jobs: install: false start: pnpm start wait-on: 'http://localhost:5678' - wait-on-timeout: 120 # + wait-on-timeout: 120 record: ${{ inputs.record }} - parallel: ${{ inputs.parallel }} + parallel: ${{ fromJSON( inputs.spec == 'e2e/*' && inputs.parallel || false ) }} # We have to provide custom ci-build-id key to make sure that this workflow could be run multiple times # in the same parent workflow ci-build-id: ${{ needs.prepare.outputs.uuid }} @@ -148,3 +155,22 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} E2E_TESTS: true COMMIT_INFO_MESSAGE: 🌳 ${{ inputs.branch }} 🖥️ ${{ inputs.run-env }} 🤖 ${{ inputs.user }} 🗃️ ${{ inputs.spec }} + + # Check if all tests passed and set the output variable + check_testing_matrix: + runs-on: ubuntu-latest + needs: [testing] + outputs: + all_tests_passed: ${{ steps.all_tests_passed.outputs.result }} + steps: + - name: Check all tests passed + id: all_tests_passed + run: | + success=true + for status in ${{ needs.testing.result }}; do + if [ $status != "success" ]; then + success=false + break + fi + done + echo "::set-output name=result::$success" diff --git a/.github/workflows/e2e-tests-pr.yml b/.github/workflows/e2e-tests-pr.yml index e73f02aa804a8..648383750d1ab 100644 --- a/.github/workflows/e2e-tests-pr.yml +++ b/.github/workflows/e2e-tests-pr.yml @@ -26,7 +26,7 @@ jobs: if: always() steps: - name: E2E success comment - if: ${{!contains(github.event.pull_request.labels.*.name, 'community') || needs.run-e2e-tests.result == 'success' }} + if: ${{!contains(github.event.pull_request.labels.*.name, 'community') && needs.run-e2e-tests.outputs.tests_passed == 'true' }} uses: peter-evans/create-or-update-comment@v3 with: issue-number: ${{ github.event.pull_request.number }} From a19d4447ac38e40d1fd1da83beb6c20fb7b2d0ed Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Fri, 21 Apr 2023 14:08:51 +0200 Subject: [PATCH 007/110] fix(editor): Resolve expressions for grandparent nodes (#5859) * fix(editor): Resolve expressions for grandparent nodes * test: add tests * test: add tests for bug * test: add todos * test: lintfix * test: add small waits * test: add linking tests * test: add test for branch mapping * test: update workflow values * test: comment out test * test: fix up tests with new values * chore: remove todos * test: add ticket number for broken test * test: refactor a bit * test: uncomment * test: fix mapping test * fix: lint issue * test: split tests * Revert "test: split tests" 0290d51d7c983320a718346ccb80fbad93894c6e * test: update mousedown * test: split up tests * test: fix test * test: fix test * test: make less flaky * test: make less flaky * test: enable teset --- cypress/e2e/14-mapping.cy.ts | 2 +- cypress/e2e/24-ndv-paired-item.cy.ts | 288 +++++++++++++++++ cypress/e2e/5-ndv.cy.ts | 1 + cypress/fixtures/Test_workflow_5.json | 292 ++++++++++++++++++ cypress/pages/ndv.ts | 30 ++ .../components/ExpressionParameterInput.vue | 6 +- .../src/components/ParameterInputWrapper.vue | 30 +- packages/editor-ui/src/components/RunData.vue | 13 +- .../editor-ui/src/components/RunDataTable.vue | 1 + .../editor-ui/src/mixins/expressionManager.ts | 19 +- .../editor-ui/src/mixins/workflowHelpers.ts | 79 +---- packages/editor-ui/src/stores/ndv.ts | 9 + packages/editor-ui/src/stores/workflows.ts | 89 +++++- 13 files changed, 759 insertions(+), 100 deletions(-) create mode 100644 cypress/e2e/24-ndv-paired-item.cy.ts create mode 100644 cypress/fixtures/Test_workflow_5.json diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 69487f699ea06..8c4d6f9cdbe05 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -205,7 +205,7 @@ describe('Data mapping', () => { 'have.text', `{{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input[0].count }} {{ $node['${SCHEDULE_TRIGGER_NODE_NAME}'].json.input }}`, ); - ndv.getters.parameterExpressionPreview('value').should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '[empty]'); ndv.actions.selectInputNode('Set'); diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts new file mode 100644 index 0000000000000..133cc808bb626 --- /dev/null +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -0,0 +1,288 @@ +import { WorkflowPage, NDV } from '../pages'; +import { v4 as uuid } from 'uuid'; + +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); + +describe('NDV', () => { + before(() => { + cy.resetAll(); + cy.skipSetup(); + + }); + beforeEach(() => { + workflowPage.actions.visit(); + workflowPage.actions.renameWorkflow(uuid()); + workflowPage.actions.saveWorkflowOnButtonClick(); + }); + + it('maps paired input and output items', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeWorkflow(); + + workflowPage.actions.openNode('Item Lists'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputPanel().contains('6 items').should('exist'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + // input to output + ndv.getters.inputTableRow(1) + .should('exist') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(1) + .realHover(); + ndv.getters.outputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(2) + .realHover(); + ndv.getters.outputTableRow(2) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.inputTableRow(3) + .realHover(); + ndv.getters.outputTableRow(6) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + // output to input + ndv.getters.outputTableRow(1) + .realHover(); + ndv.getters.inputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(4) + .realHover(); + ndv.getters.inputTableRow(1) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(2) + .realHover(); + ndv.getters.inputTableRow(2) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(6) + .realHover(); + ndv.getters.inputTableRow(3) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(1) + .realHover(); + ndv.getters.inputTableRow(4) + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + }); + + it('maps paired input and output items based on selected input node', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set2'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputRunSelector() + .should('exist') + .should('include.text', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + cy.wait(50); + + ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + + ndv.actions.selectInputNode('Set1'); + ndv.getters.inputHoveringItem().should('have.text', '1000').realHover(); + ndv.getters.outputHoveringItem().should('have.text', '1000'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('Item Lists'); + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputHoveringItem().should('have.text', '1111').realHover(); + ndv.getters.outputHoveringItem().should('have.text', '1111'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + }); + + it('maps paired input and output items based on selected run', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set3'); + + ndv.getters.inputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + ndv.getters.outputRunSelector() + .should('exist') + .find('input') + .should('include.value', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.changeOutputRunSelector('1 of 2 (6 items)'); + ndv.getters.inputRunSelector().find('input') + .should('include.value', '1 of 2 (6 items)'); + ndv.getters.outputRunSelector().find('input') + .should('include.value', '1 of 2 (6 items)'); + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputTableRow(1) + .should('have.text', '1111') + .realHover(); + + ndv.getters.outputTableRow(3) + .should('have.text', '4444') + .realHover(); + ndv.getters.inputTableRow(3) + .should('have.text', '4444') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.actions.changeOutputRunSelector('2 of 2 (6 items)'); + cy.wait(50); + + ndv.getters.inputTableRow(1) + .should('have.text', '1000') + .realHover(); + ndv.getters.outputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(3) + .should('have.text', '2000') + .realHover(); + ndv.getters.inputTableRow(3) + .should('have.text', '2000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + }); + + it('resolves expression with default item when input node is not parent, while still pairing items', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Set2'); + + ndv.getters.inputPanel().contains('6 items').should('exist'); + ndv.getters.outputRunSelector() + .should('exist') + .should('include.text', '2 of 2 (6 items)'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.getters.backToCanvas().realHover(); // reset to default hover + ndv.getters.inputHoveringItem().should('have.text', '1111').realHover(); + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1111'); + + ndv.actions.selectInputNode('Code1'); + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1) + .should('have.text', '1000') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputTableRow(1) + .should('have.text', '1000'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('Code'); + + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1) + .should('have.text', '6666') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + ndv.getters.outputHoveringItem().should('not.exist'); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + + ndv.actions.selectInputNode('When clicking'); + + ndv.getters.inputTableRow(1).realHover(); + ndv.getters.inputTableRow(1).should('have.text', "This is an item, but it's empty.").realHover(); + ndv.getters.outputHoveringItem().should('have.length', 6); + ndv.getters.parameterExpressionPreview('value').should('include.text', '1000'); + }); + + it('can pair items between input and output across branches and runs', () => { + cy.fixture('Test_workflow_5.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('IF'); + + ndv.actions.switchInputMode('Table'); + ndv.actions.switchOutputMode('Table'); + + ndv.actions.switchOutputBranch('False Branch (2 items)'); + ndv.getters.outputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.inputTableRow(5) + .should('have.text', '8888') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.getters.outputTableRow(2) + .should('have.text', '9999') + .realHover(); + ndv.getters.inputTableRow(6) + .should('have.text', '9999') + .invoke('attr', 'data-test-id') + .should('equal', 'hovering-item'); + + ndv.actions.close(); + workflowPage.actions.openNode('Set5'); + ndv.getters.outputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.inputHoveringItem().should('not.exist'); + + ndv.getters.inputTableRow(1) + .should('have.text', '1111') + .realHover(); + ndv.getters.outputHoveringItem().should('not.exist'); + + ndv.actions.switchIntputBranch('False Branch'); + ndv.getters.inputTableRow(1) + .should('have.text', '8888') + .realHover(); + ndv.getters.outputHoveringItem().should('have.text', '8888'); + + ndv.actions.changeOutputRunSelector('1 of 2 (4 items)') + ndv.getters.outputTableRow(1) + .should('have.text', '1111') + .realHover(); + // todo there's a bug here need to fix ADO-534 + // ndv.getters.outputHoveringItem().should('not.exist'); + }); +}); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index ea4533725cc10..c9c82e50fac11 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -15,6 +15,7 @@ describe('NDV', () => { workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.saveWorkflowOnButtonClick(); }); + it('should show up when double clicked on a node and close when Back to canvas clicked', () => { workflowPage.actions.addInitialNodeToCanvas('Manual'); workflowPage.getters.canvasNodes().first().dblclick(); diff --git a/cypress/fixtures/Test_workflow_5.json b/cypress/fixtures/Test_workflow_5.json new file mode 100644 index 0000000000000..6b87fc33b70a1 --- /dev/null +++ b/cypress/fixtures/Test_workflow_5.json @@ -0,0 +1,292 @@ +{ + "meta": { + "instanceId": "8147b3a74cd161276e0f3bfc17369a724afab0d377593fada8be82d34c0c6a95" + }, + "nodes": [ + { + "parameters": { + "jsCode": "return [\n {\n id: 6666\n },\n {\n id: 3333\n },\n {\n id: 9999\n },\n {\n id: 1111\n },\n {\n id: 4444\n },\n {\n id: 8888\n },\n]" + }, + "id": "5f023c7c-67ca-47a0-8a90-8227fcf29b9c", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + -520, + 580 + ] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "bd454282-9dd7-465f-9b9a-654a0c8532ec", + "name": "Set2", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -40, + 780 + ] + }, + { + "parameters": {}, + "id": "ef63cdc5-50bc-4525-9873-7e7f7589a60e", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -740, + 580 + ] + }, + { + "parameters": { + "operation": "sort", + "sortFieldsUi": { + "sortField": [ + { + "fieldName": "id" + } + ] + }, + "options": {} + }, + "id": "555a150c-d735-4331-b628-c1f1cfed2da1", + "name": "Item Lists", + "type": "n8n-nodes-base.itemLists", + "typeVersion": 2, + "position": [ + -280, + 580 + ] + }, + { + "parameters": { + "values": { + "string": [ + { + "name": "id", + "value": "={{ $json.id }}" + } + ] + }, + "options": {} + }, + "id": "02372cb6-aac8-45c3-8600-f699901289ac", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -60, + 580 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "00d73944-218c-4896-af68-3f2855a922d1", + "name": "Set1", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + -280, + 780 + ] + }, + { + "parameters": { + "conditions": { + "number": [ + { + "value1": "={{ $json.id }}", + "operation": "smallerEqual", + "value2": 6666 + } + ] + } + }, + "id": "211a7bef-32d1-4928-9cef-3a45f2e61379", + "name": "IF", + "type": "n8n-nodes-base.if", + "typeVersion": 1, + "position": [ + 160, + 580 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "dcbd4745-832f-43d8-8a3c-dd80e8ca2777", + "name": "Set3", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 140, + 780 + ] + }, + { + "parameters": { + "jsCode": "return [\n {\n id: 1000\n },\n {\n id: 300\n },\n {\n id: 2000\n },\n {\n id: 100\n },\n {\n id: 400\n },\n {\n id: 1300\n },\n]" + }, + "id": "ec9c8f16-f3c8-4054-a6e9-4f1ebcdebb71", + "name": "Code1", + "type": "n8n-nodes-base.code", + "typeVersion": 1, + "position": [ + -520, + 780 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "42e89478-a53a-4d10-b20c-1dc5d5f953d5", + "name": "Set4", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 460, + 460 + ] + }, + { + "parameters": { + "options": {} + }, + "id": "5085eb1c-0345-4b9d-856a-2955279f2c5d", + "name": "Set5", + "type": "n8n-nodes-base.set", + "typeVersion": 2, + "position": [ + 460, + 660 + ] + } + ], + "connections": { + "Code": { + "main": [ + [ + { + "node": "Item Lists", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set2": { + "main": [ + [ + { + "node": "Set3", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + }, + { + "node": "Code1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Item Lists": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + }, + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set": { + "main": [ + [ + { + "node": "IF", + "type": "main", + "index": 0 + } + ] + ] + }, + "Set1": { + "main": [ + [ + { + "node": "Set2", + "type": "main", + "index": 0 + } + ] + ] + }, + "IF": { + "main": [ + [ + { + "node": "Set4", + "type": "main", + "index": 0 + }, + { + "node": "Set5", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "Set5", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code1": { + "main": [ + [ + { + "node": "Set1", + "type": "main", + "index": 0 + } + ] + ] + } + } +} \ No newline at end of file diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 7ce0c811fee97..15f177240f793 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -45,6 +45,12 @@ export class NDV extends BasePage { executePrevious: () => cy.getByTestId('execute-previous-node'), httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'), nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n), + inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'), + outputRunSelector: () => this.getters.outputPanel().findChildByTestId('run-selector'), + outputHoveringItem: () => this.getters.outputPanel().findChildByTestId('hovering-item'), + inputHoveringItem: () => this.getters.inputPanel().findChildByTestId('hovering-item'), + outputBranches: () => this.getters.outputPanel().findChildByTestId('branches'), + inputBranches: () => this.getters.inputPanel().findChildByTestId('branches'), }; actions = { @@ -119,5 +125,29 @@ export class NDV extends BasePage { this.actions.editPinnedData(); this.actions.savePinnedData(); }, + changeInputRunSelector: (runName: string) => { + this.getters.inputRunSelector().click(); + cy.get('.el-select-dropdown:visible .el-select-dropdown__item') + .contains(runName) + .click(); + }, + changeOutputRunSelector: (runName: string) => { + this.getters.outputRunSelector().click(); + cy.get('.el-select-dropdown:visible .el-select-dropdown__item') + .contains(runName) + .click(); + }, + toggleOutputRunLinking: () => { + this.getters.outputRunSelector().find('button').click(); + }, + toggleInputRunLinking: () => { + this.getters.inputRunSelector().find('button').click(); + }, + switchOutputBranch: (name: string) => { + this.getters.outputBranches().get('span').contains(name).click(); + }, + switchIntputBranch: (name: string) => { + this.getters.inputBranches().get('span').contains(name).click(); + }, }; } diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue index 51e58eef9dc9d..3a8b6e79a5804 100644 --- a/packages/editor-ui/src/components/ExpressionParameterInput.vue +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -115,7 +115,11 @@ export default Vue.extend({ return (this.hoveringItem?.itemIndex ?? 0) + 1; }, hoveringItem(): TargetItem | null { - return this.ndvStore.hoveringItem; + if (this.ndvStore.isInputParentOfActiveNode) { + return this.ndvStore.hoveringItem; + } + + return null; }, isDragging(): boolean { return this.ndvStore.isDraggableDragging; diff --git a/packages/editor-ui/src/components/ParameterInputWrapper.vue b/packages/editor-ui/src/components/ParameterInputWrapper.vue index d1f2c73ebb164..86e6ad9acba46 100644 --- a/packages/editor-ui/src/components/ParameterInputWrapper.vue +++ b/packages/editor-ui/src/components/ParameterInputWrapper.vue @@ -28,7 +28,7 @@ :class="$style.hint" data-test-id="parameter-expression-preview" class="ph-no-capture" - :highlight="!!(expressionOutput && targetItem)" + :highlight="!!(expressionOutput && targetItem) && isInputParentOfActiveNode" :hint="expressionOutput" :singleLine="true" /> @@ -153,25 +153,29 @@ export default mixins(showMessage, workflowHelpers).extend({ targetItem(): TargetItem | null { return this.ndvStore.hoveringItem; }, + isInputParentOfActiveNode(): boolean { + return this.ndvStore.isInputParentOfActiveNode; + }, expressionValueComputed(): string | null { - const inputNodeName: string | undefined = this.ndvStore.ndvInputNodeName; const value = isResourceLocatorValue(this.value) ? this.value.value : this.value; - if (this.activeNode === null || !this.isValueExpression || typeof value !== 'string') { + if (!this.activeNode || !this.isValueExpression || typeof value !== 'string') { return null; } - const inputRunIndex: number | undefined = this.ndvStore.ndvInputRunIndex; - const inputBranchIndex: number | undefined = this.ndvStore.ndvInputBranchIndex; - let computedValue: NodeParameterValue; try { - const targetItem = this.targetItem ?? undefined; - computedValue = this.resolveExpression(value, undefined, { - targetItem, - inputNodeName, - inputRunIndex, - inputBranchIndex, - }); + let opts; + if (this.ndvStore.isInputParentOfActiveNode) { + opts = { + targetItem: this.targetItem ?? undefined, + inputNodeName: this.ndvStore.ndvInputNodeName, + inputRunIndex: this.ndvStore.ndvInputRunIndex, + inputBranchIndex: this.ndvStore.ndvInputBranchIndex, + }; + } + + computedValue = this.resolveExpression(value, undefined, opts); + if (computedValue === null) { return null; } diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index eb31cf17b13f3..8a97746285bc9 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -121,7 +121,12 @@ -
+
-
+
diff --git a/packages/editor-ui/src/components/RunDataTable.vue b/packages/editor-ui/src/components/RunDataTable.vue index 07cd4d134d166..453f71cf3dead 100644 --- a/packages/editor-ui/src/components/RunDataTable.vue +++ b/packages/editor-ui/src/components/RunDataTable.vue @@ -107,6 +107,7 @@ v-for="(row, index1) in tableData.data" :key="index1" :class="{ [$style.hoveringRow]: isHoveringRow(index1) }" + :data-test-id="isHoveringRow(index1) ? 'hovering-item' : undefined" > => {}, - // @ts-ignore - getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { - const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); - - if (nodeTypeDescription === null) { - return undefined; - } - - return { - description: nodeTypeDescription, - // As we do not have the trigger/poll functions available in the frontend - // we use the information available to figure out what are trigger nodes - // @ts-ignore - trigger: - (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && - nodeTypeDescription.inputs.length === 0 && - !nodeTypeDescription.webhooks) || - undefined, - }; - }, - }; - - return nodeTypes; + return useWorkflowsStore().getNodeTypes(); } // Returns connectionInputData to be able to execute an expression. diff --git a/packages/editor-ui/src/stores/ndv.ts b/packages/editor-ui/src/stores/ndv.ts index fed4fc96ea045..caf76b11d9680 100644 --- a/packages/editor-ui/src/stores/ndv.ts +++ b/packages/editor-ui/src/stores/ndv.ts @@ -111,6 +111,15 @@ export const useNDVStore = defineStore(STORES.NDV, { isDNVDataEmpty() { return (panel: 'input' | 'output'): boolean => this[panel].data.isEmpty; }, + isInputParentOfActiveNode(): boolean { + const inputNodeName = this.ndvInputNodeName; + if (!this.activeNode || !inputNodeName) { + return false; + } + const workflow = useWorkflowsStore().getCurrentWorkflow(); + const parentNodes = workflow.getParentNodes(this.activeNode.name, 'main', 1); + return parentNodes.includes(inputNodeName); + }, }, actions: { setInputNodeName(name: string | undefined): void { diff --git a/packages/editor-ui/src/stores/workflows.ts b/packages/editor-ui/src/stores/workflows.ts index 2b1853d1495a0..3d9600c0c851a 100644 --- a/packages/editor-ui/src/stores/workflows.ts +++ b/packages/editor-ui/src/stores/workflows.ts @@ -2,8 +2,10 @@ import { DEFAULT_NEW_WORKFLOW_NAME, DUPLICATE_POSTFFIX, EnterpriseEditionFeature, + ERROR_TRIGGER_NODE_TYPE, MAX_WORKFLOW_NAME_LENGTH, PLACEHOLDER_EMPTY_WORKFLOW_ID, + START_NODE_TYPE, STORES, } from '@/constants'; import { @@ -36,6 +38,8 @@ import { INodeExecutionData, INodeIssueData, INodeParameters, + INodeTypeData, + INodeTypes, IPinData, IRun, IRunData, @@ -43,6 +47,7 @@ import { ITaskData, IWorkflowSettings, NodeHelpers, + Workflow, } from 'n8n-workflow'; import Vue from 'vue'; @@ -86,6 +91,9 @@ const createEmptyWorkflow = (): IWorkflowDb => ({ usedCredentials: [], }); +let cachedWorkflowKey: string | null = ''; +let cachedWorkflow: Workflow | null = null; + export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { state: (): WorkflowsState => ({ workflow: createEmptyWorkflow(), @@ -256,7 +264,86 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { }, }, actions: { - // Workflow actions + getNodeTypes(): INodeTypes { + const nodeTypes: INodeTypes = { + nodeTypes: {}, + init: async (nodeTypes?: INodeTypeData): Promise => {}, + // @ts-ignore + getByNameAndVersion: (nodeType: string, version?: number): INodeType | undefined => { + const nodeTypeDescription = useNodeTypesStore().getNodeType(nodeType, version); + + if (nodeTypeDescription === null) { + return undefined; + } + + return { + description: nodeTypeDescription, + // As we do not have the trigger/poll functions available in the frontend + // we use the information available to figure out what are trigger nodes + // @ts-ignore + trigger: + (![ERROR_TRIGGER_NODE_TYPE, START_NODE_TYPE].includes(nodeType) && + nodeTypeDescription.inputs.length === 0 && + !nodeTypeDescription.webhooks) || + undefined, + }; + }, + }; + + return nodeTypes; + }, + + // Returns a shallow copy of the nodes which means that all the data on the lower + // levels still only gets referenced but the top level object is a different one. + // This has the advantage that it is very fast and does not cause problems with vuex + // when the workflow replaces the node-parameters. + getNodes(): INodeUi[] { + const nodes = useWorkflowsStore().allNodes; + const returnNodes: INodeUi[] = []; + + for (const node of nodes) { + returnNodes.push(Object.assign({}, node)); + } + + return returnNodes; + }, + + // Returns a workflow instance. + getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow { + const nodeTypes = this.getNodeTypes(); + let workflowId: string | undefined = useWorkflowsStore().workflowId; + if (workflowId && workflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) { + workflowId = undefined; + } + + const workflowName = useWorkflowsStore().workflowName; + + cachedWorkflow = new Workflow({ + id: workflowId, + name: workflowName, + nodes: copyData ? deepCopy(nodes) : nodes, + connections: copyData ? deepCopy(connections) : connections, + active: false, + nodeTypes, + settings: useWorkflowsStore().workflowSettings, + // @ts-ignore + pinData: useWorkflowsStore().getPinData, + }); + + return cachedWorkflow; + }, + + getCurrentWorkflow(copyData?: boolean): Workflow { + const nodes = this.getNodes(); + const connections = this.allConnections; + const cacheKey = JSON.stringify({ nodes, connections }); + if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) { + return cachedWorkflow; + } + cachedWorkflowKey = cacheKey; + + return this.getWorkflow(nodes, connections, copyData); + }, async fetchAllWorkflows(): Promise { const rootStore = useRootStore(); From dc2a7a307a9156daf4a4358554ce12ff1dbd5fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Fri, 21 Apr 2023 15:09:56 +0200 Subject: [PATCH 008/110] refactor: Patch to adjust `consistent-type-imports` (no-changelog) (#6057) :package: Patch dependency --- package.json | 3 ++- .../@typescript-eslint__eslint-plugin@5.45.0.patch | 13 +++++++++++++ pnpm-lock.yaml | 12 ++++++++---- 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 patches/@typescript-eslint__eslint-plugin@5.45.0.patch diff --git a/package.json b/package.json index df49c446bd138..d8e16fce32937 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,8 @@ "patchedDependencies": { "element-ui@2.15.12": "patches/element-ui@2.15.12.patch", "typedi@0.10.0": "patches/typedi@0.10.0.patch", - "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch" + "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", + "@typescript-eslint/eslint-plugin@5.45.0": "patches/@typescript-eslint__eslint-plugin@5.45.0.patch" } } } diff --git a/patches/@typescript-eslint__eslint-plugin@5.45.0.patch b/patches/@typescript-eslint__eslint-plugin@5.45.0.patch new file mode 100644 index 0000000000000..9fe89a07c3080 --- /dev/null +++ b/patches/@typescript-eslint__eslint-plugin@5.45.0.patch @@ -0,0 +1,13 @@ +diff --git a/dist/rules/consistent-type-imports.js b/dist/rules/consistent-type-imports.js +index fe6eaf80a1285b62ed93b0c32b74c889788c5164..de4e2ca30c131948e030b7d6dbce4ff50e3eff26 100644 +--- a/dist/rules/consistent-type-imports.js ++++ b/dist/rules/consistent-type-imports.js +@@ -87,6 +87,8 @@ exports.default = util.createRule({ + ImportDeclaration(node) { + var _a; + const source = node.source.value; ++ if (source.endsWith('.vue')) return; ++ + // sourceImports is the object containing all the specifics for a particular import source, type or value + const sourceImports = (_a = sourceImportsMap[source]) !== null && _a !== void 0 ? _a : (sourceImportsMap[source] = { + source, \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab2f019ed9351..d37180405f19d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ patchedDependencies: '@sentry/cli@2.17.0': hash: nchnoezkq6p37qaiku3vrpwraq path: patches/@sentry__cli@2.17.0.patch + '@typescript-eslint/eslint-plugin@5.45.0': + hash: dd54h3bsflbrys5h3bbkqmbvea + path: patches/@typescript-eslint__eslint-plugin@5.45.0.patch element-ui@2.15.12: hash: prckukfdop5sl2her6de25cod4 path: patches/element-ui@2.15.12.patch @@ -117,7 +120,7 @@ importers: version: 8.4.6 '@typescript-eslint/eslint-plugin': specifier: ~5.45 - version: 5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) + version: 5.45.0(patch_hash=dd54h3bsflbrys5h3bbkqmbvea)(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) '@typescript-eslint/parser': specifier: ~5.45 version: 5.45.0(eslint@8.28.0)(typescript@5.0.3) @@ -7042,7 +7045,7 @@ packages: dev: true optional: true - /@typescript-eslint/eslint-plugin@5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3): + /@typescript-eslint/eslint-plugin@5.45.0(patch_hash=dd54h3bsflbrys5h3bbkqmbvea)(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3): resolution: {integrity: sha512-CXXHNlf0oL+Yg021cxgOdMHNTXD17rHkq7iW6RFHoybdFgQBjU3yIXhhcPpGwr1CjZlo6ET8C6tzX5juQoXeGA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: @@ -7068,6 +7071,7 @@ packages: transitivePeerDependencies: - supports-color dev: true + patched: true /@typescript-eslint/parser@5.45.0(eslint@8.28.0)(typescript@5.0.3): resolution: {integrity: sha512-brvs/WSM4fKUmF5Ot/gEve6qYiCMjm6w4HkHPfS6ZNmxTS0m0iNN4yOChImaCkqc1hRwFGqUyanMXuGal6oyyQ==} @@ -7441,7 +7445,7 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) + '@typescript-eslint/eslint-plugin': 5.45.0(patch_hash=dd54h3bsflbrys5h3bbkqmbvea)(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) eslint: 8.28.0 eslint-plugin-vue: 7.17.0(eslint@8.28.0) @@ -11133,7 +11137,7 @@ packages: eslint: ^7.32.0 || ^8.2.0 eslint-plugin-import: ^2.25.3 dependencies: - '@typescript-eslint/eslint-plugin': 5.45.0(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) + '@typescript-eslint/eslint-plugin': 5.45.0(patch_hash=dd54h3bsflbrys5h3bbkqmbvea)(@typescript-eslint/parser@5.45.0)(eslint@8.28.0)(typescript@5.0.3) '@typescript-eslint/parser': 5.45.0(eslint@8.28.0)(typescript@5.0.3) eslint: 8.28.0 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.26.0)(eslint@8.28.0) From 649389edad3d0709f8ab5ca699eef77c8450f9d3 Mon Sep 17 00:00:00 2001 From: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> Date: Fri, 21 Apr 2023 15:37:09 +0200 Subject: [PATCH 009/110] test: Add stickies tests (#5413) * test: Add tests for stickies * test: add sticky basic test * test: add size dragging tests * test: add delete sticky test * test: add editing test * test: update editing text * test: add expansion tests * test: add more tests * test: clean up tests * refactor: update dragging tests to make sense * refactor: upate drag right test * test: add shrink from right test * test: refactor some more * test: fix all tests * test: clean up * test: update number * test: add z-index tests * test: address comments * test: fix mistake * test: wait on save * test: try button instead --- cypress/e2e/25-stickies.cy.ts | 262 ++++++++++++++++++ cypress/pages/workflow.ts | 22 ++ cypress/support/commands.ts | 11 +- cypress/support/index.ts | 2 +- .../src/components/Node/NodeCreation.vue | 1 + packages/editor-ui/src/components/Sticky.vue | 8 +- 6 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 cypress/e2e/25-stickies.cy.ts diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts new file mode 100644 index 0000000000000..0746fddc0326a --- /dev/null +++ b/cypress/e2e/25-stickies.cy.ts @@ -0,0 +1,262 @@ +import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; + +const workflowPage = new WorkflowPageClass(); + +function checkStickiesStyle( top: number, left: number, height: number, width: number, zIndex?: number) { + workflowPage.getters.stickies().should(($el) => { + expect($el).to.have.css('top', `${top}px`); + expect($el).to.have.css('left', `${left}px`); + expect($el).to.have.css('height', `${height}px`); + expect($el).to.have.css('width', `${width}px`); + if (zIndex) { + expect($el).to.have.css('z-index', `${zIndex}`); + } + }); +} + +describe('Canvas Actions', () => { + beforeEach(() => { + cy.resetAll(); + cy.skipSetup(); + workflowPage.actions.visit(); + + cy.window().then( + (win) => { + // @ts-ignore + win.preventNodeViewBeforeUnload = true; + }, + ); + }); + + + it('adds sticky to canvas with default text and position', () => { + workflowPage.getters.addStickyButton().should('not.be.visible'); + + addDefaultSticky() + workflowPage.getters.stickies().eq(0) + .should('have.text', 'I’m a note\nDouble click to edit me. Guide\n') + .find('a').contains('Guide').should('have.attr', 'href'); + }); + + it('drags sticky around to top left corner', () => { + // used to caliberate move sticky function + addDefaultSticky(); + moveSticky({ top: 0, left: 0 }); + }); + + it('drags sticky around and position/size are saved correctly', () => { + addDefaultSticky(); + moveSticky({ top: 500, left: 500 }); + + workflowPage.actions.saveWorkflowOnButtonClick(); + cy.wait('@createWorkflow'); + + cy.reload(); + cy.waitForLoad(); + + stickyShouldBePositionedCorrectly({ top: 500, left: 500 }); + }); + + it('deletes sticky', () => { + workflowPage.actions.addSticky(); + workflowPage.getters.stickies().should('have.length', 1) + + workflowPage.actions.deleteSticky(); + + workflowPage.getters.stickies().should('have.length', 0) + }); + + it('edits sticky and updates content as markdown', () => { + workflowPage.actions.addSticky(); + + workflowPage.getters.stickies() + .should('have.text', 'I’m a note\nDouble click to edit me. Guide\n') + + workflowPage.getters.stickies().dblclick(); + workflowPage.actions.editSticky('# hello world \n ## text text'); + workflowPage.getters.stickies().find('h1').should('have.text', 'hello world'); + workflowPage.getters.stickies().find('h2').should('have.text', 'text text'); + }); + + it('expands/shrinks sticky from the right edge', () => { + addDefaultSticky(); + + moveSticky({ top: 200, left: 200 }); + + dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, 100); + dragRightEdge({ left: 200, top: 200, height: 160, width: 240 }, -50); + }); + + it('expands/shrinks sticky from the left edge', () => { + addDefaultSticky(); + + moveSticky({ left: 600, top: 200 }); + cy.drag('[data-test-id="sticky"] [data-dir="left"]', [100, 100]); + checkStickiesStyle(140, 510, 160, 150); + + cy.drag('[data-test-id="sticky"] [data-dir="left"]', [-50, -50]); + checkStickiesStyle(140, 466, 160, 194); + }); + + it('expands/shrinks sticky from the top edge', () => { + workflowPage.actions.addSticky(); + cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button + checkStickiesStyle(360, 620, 160, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="top"]', [100, 100]); + checkStickiesStyle(440, 620, 80, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="top"]', [-50, -50]); + checkStickiesStyle(384, 620, 136, 240); + }); + + it('expands/shrinks sticky from the bottom edge', () => { + workflowPage.actions.addSticky(); + cy.drag('[data-test-id="sticky"]', [100, 100]); // move away from canvas button + checkStickiesStyle(360, 620, 160, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [100, 100]); + checkStickiesStyle(360, 620, 254, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="bottom"]', [-50, -50]); + checkStickiesStyle(360, 620, 198, 240); + }); + + it('expands/shrinks sticky from the bottom right edge', () => { + workflowPage.actions.addSticky(); + cy.drag('[data-test-id="sticky"]', [-100, -100]); // move away from canvas button + checkStickiesStyle(160, 420, 160, 240); + + cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [100, 100]); + checkStickiesStyle(160, 420, 254, 346); + + cy.drag('[data-test-id="sticky"] [data-dir="bottomRight"]', [-50, -50]); + checkStickiesStyle(160, 420, 198, 302); + }); + + it('expands/shrinks sticky from the top right edge', () => { + addDefaultSticky(); + + cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [100, 100]); + checkStickiesStyle(420, 400, 80, 346); + + cy.drag('[data-test-id="sticky"] [data-dir="topRight"]', [-50, -50]); + checkStickiesStyle(364, 400, 136, 302); + }); + + it('expands/shrinks sticky from the top left edge, and reach min height/width', () => { + addDefaultSticky(); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [100, 100]); + checkStickiesStyle(420, 490, 80, 150); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); + checkStickiesStyle(264, 346, 236, 294); + }); + + it('sets sticky behind node', () => { + workflowPage.actions.addInitialNodeToCanvas('Manual Trigger'); + addDefaultSticky(); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-150, -150]); + checkStickiesStyle(184, 256, 316, 384, -121); + + workflowPage.getters.canvasNodes().eq(0) + .should(($el) => { + expect($el).to.have.css('z-index', 'auto'); + }); + + workflowPage.actions.addSticky(); + workflowPage.getters.stickies().eq(0) + .should(($el) => { + expect($el).to.have.css('z-index', '-121'); + }); + workflowPage.getters.stickies().eq(1) + .should(($el) => { + expect($el).to.have.css('z-index', '-38'); + }); + + cy.drag('[data-test-id="sticky"] [data-dir="topLeft"]', [-200, -200], { index: 1 }); + workflowPage.getters.stickies().eq(0) + .should(($el) => { + expect($el).to.have.css('z-index', '-121'); + }); + + workflowPage.getters.stickies().eq(1) + .should(($el) => { + expect($el).to.have.css('z-index', '-158'); + }); + + }); +}); + +type Position = { + top: number; + left: number; +}; + +type BoundingBox = { + height: number; + width: number; + top: number; + left: number; +} + +function dragRightEdge(curr: BoundingBox, move: number) { + workflowPage.getters.stickies().first().then(($el) => { + const { left, top, height, width } = curr; + cy.drag(`[data-test-id="sticky"] [data-dir="right"]`, [left + width + move, 0], { abs: true }); + stickyShouldBePositionedCorrectly({ top, left }); + stickyShouldHaveCorrectSize([height, width * 1.5 + move]); + }); +} + +function shouldHaveOneSticky() { + workflowPage.getters.stickies().should('have.length', 1); +} + +function shouldBeInDefaultLocation() { + workflowPage.getters.stickies().eq(0).should(($el) => { + expect($el).to.have.css('height', '160px'); + expect($el).to.have.css('width', '240px'); + }) +} + +function shouldHaveDefaultSize() { + workflowPage.getters.stickies().should(($el) => { + expect($el).to.have.css('height', '160px'); + expect($el).to.have.css('width', '240px'); + }) +} + +function addDefaultSticky() { + workflowPage.actions.addSticky(); + shouldHaveOneSticky(); + shouldHaveDefaultSize(); + shouldBeInDefaultLocation(); +} + +function stickyShouldBePositionedCorrectly(position: Position) { + const yOffset = -60; + const xOffset = -180; + workflowPage.getters.stickies() + .should(($el) => { + expect($el).to.have.css('top', `${yOffset + position.top}px`); + expect($el).to.have.css('left', `${xOffset + position.left}px`); + }); +} + +function stickyShouldHaveCorrectSize(size: [number, number]) { + const yOffset = 0; + const xOffset = 0; + workflowPage.getters.stickies() + .should(($el) => { + expect($el).to.have.css('height', `${yOffset + size[0]}px`); + expect($el).to.have.css('width', `${xOffset + size[1]}px`); + }); +} + +function moveSticky(target: Position) { + cy.drag('[data-test-id="sticky"]', [target.left, target.top], { abs: true }); + stickyShouldBePositionedCorrectly(target); +} diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index 4ad4b753a9161..d42b2850c4e93 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -106,6 +106,8 @@ export class WorkflowPage extends BasePage { cy.get( `.connection-actions[data-source-node="${sourceNodeName}"][data-target-node="${targetNodeName}"]`, ), + addStickyButton: () => cy.getByTestId('add-sticky-button'), + stickies: () => cy.getByTestId('sticky'), editorTabButton: () => cy.getByTestId('radio-button-workflow'), }; actions = { @@ -167,11 +169,13 @@ export class WorkflowPage extends BasePage { this.getters.shareButton().click(); }, saveWorkflowOnButtonClick: () => { + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); this.getters.saveButton().should('contain', 'Save'); this.getters.saveButton().click(); this.getters.saveButton().should('contain', 'Saved'); }, saveWorkflowUsingKeyboardShortcut: () => { + cy.intercept('POST', '/rest/workflows').as('createWorkflow'); cy.get('body').type('{meta}', { release: false }).type('s'); }, deleteNode: (name: string) => { @@ -257,6 +261,24 @@ export class WorkflowPage extends BasePage { .first() .click({ force: true }); }, + addSticky: () => { + this.getters.nodeCreatorPlusButton().realHover(); + this.getters.addStickyButton().click(); + }, + deleteSticky: () => { + this.getters.stickies().eq(0) + .realHover() + .find('[data-test-id="delete-sticky"]') + .click(); + }, + editSticky: (content: string) => { + this.getters.stickies() + .dblclick() + .find('textarea') + .clear() + .type(content) + .type('{esc}'); + }, turnOnManualExecutionSaving: () => { this.getters.workflowMenu().click(); this.getters.workflowMenuItemSettings().click(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index e096dc0aaa7cf..132745510cbfa 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -232,18 +232,19 @@ Cypress.Commands.add('paste', { prevSubject: true }, (selector, pastePayload) => }); }); -Cypress.Commands.add('drag', (selector, pos) => { +Cypress.Commands.add('drag', (selector, pos, options) => { + const index = options?.index || 0; const [xDiff, yDiff] = pos; - const element = cy.get(selector); + const element = cy.get(selector).eq(index); element.should('exist'); - const originalLocation = Cypress.$(selector)[0].getBoundingClientRect(); + const originalLocation = Cypress.$(selector)[index].getBoundingClientRect(); element.trigger('mousedown'); element.trigger('mousemove', { which: 1, - pageX: originalLocation.right + xDiff, - pageY: originalLocation.top + yDiff, + pageX: options?.abs? xDiff: originalLocation.right + xDiff, + pageY: options?.abs? yDiff: originalLocation.top + yDiff, force: true, }); element.trigger('mouseup', { force: true }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index b42db7e7171a8..7665602dafcb4 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -47,7 +47,7 @@ declare global { grantBrowserPermissions(...permissions: string[]): void; readClipboard(): Chainable; paste(pastePayload: string): void; - drag(selector: string, target: [number, number]): void; + drag(selector: string, target: [number, number], options?: {abs?: true, index?: number}): void; draganddrop(draggableSelector: string, droppableSelector: string): void; } } diff --git a/packages/editor-ui/src/components/Node/NodeCreation.vue b/packages/editor-ui/src/components/Node/NodeCreation.vue index edae0d921b58d..da2a9c2183b9b 100644 --- a/packages/editor-ui/src/components/Node/NodeCreation.vue +++ b/packages/editor-ui/src/components/Node/NodeCreation.vue @@ -17,6 +17,7 @@
-
+
From 19f540ecf9cbebf354b9fc65d7f128dfe80ae129 Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Fri, 21 Apr 2023 15:48:07 +0200 Subject: [PATCH 010/110] refactor(editor): Turn titleChange mixin to composable (#6059) --- .../components/MainHeader/WorkflowDetails.vue | 12 +++++--- .../editor-ui/src/components/MainSidebar.vue | 4 +-- .../src/composables/useTitleChange.ts | 19 +++++++++++++ .../editor-ui/src/mixins/pushConnection.ts | 14 ++++++---- packages/editor-ui/src/mixins/titleChange.ts | 28 ------------------- packages/editor-ui/src/mixins/workflowRun.ts | 21 +++++++------- packages/editor-ui/src/views/NodeView.vue | 15 +++++----- 7 files changed, 54 insertions(+), 59 deletions(-) create mode 100644 packages/editor-ui/src/composables/useTitleChange.ts delete mode 100644 packages/editor-ui/src/mixins/titleChange.ts diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index ca875836adb59..44a5027f63687 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -148,7 +148,7 @@ import BreakpointsObserver from '@/components/BreakpointsObserver.vue'; import { IUser, IWorkflowDataUpdate, IWorkflowDb, IWorkflowToShare } from '@/Interface'; import { saveAs } from 'file-saver'; -import { titleChange } from '@/mixins/titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import type { MessageBoxInputData } from 'element-ui/types/message-box'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; @@ -159,7 +159,6 @@ import { useTagsStore } from '@/stores/tags'; import { getWorkflowPermissions, IPermissions } from '@/permissions'; import { useUsersStore } from '@/stores/users'; import { useUsageStore } from '@/stores/usage'; -import { BaseTextKey } from '@/plugins/i18n'; import { createEventBus } from '@/event-bus'; const hasChanged = (prev: string[], curr: string[]) => { @@ -171,7 +170,7 @@ const hasChanged = (prev: string[], curr: string[]) => { return curr.reduce((accu, val) => accu || !set.has(val), false); }; -export default mixins(workflowHelpers, titleChange).extend({ +export default mixins(workflowHelpers).extend({ name: 'WorkflowDetails', components: { TagsContainer, @@ -183,6 +182,11 @@ export default mixins(workflowHelpers, titleChange).extend({ InlineTextEdit, BreakpointsObserver, }, + setup() { + return { + ...useTitleChange(), + }; + }, data() { return { isTagsEditEnabled: false, @@ -515,7 +519,7 @@ export default mixins(workflowHelpers, titleChange).extend({ } this.uiStore.stateIsDirty = false; // Reset tab title since workflow is deleted. - this.$titleReset(); + this.titleReset(); this.$showMessage({ title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'), type: 'success', diff --git a/packages/editor-ui/src/components/MainSidebar.vue b/packages/editor-ui/src/components/MainSidebar.vue index 8809d660a259a..942e1119b067c 100644 --- a/packages/editor-ui/src/components/MainSidebar.vue +++ b/packages/editor-ui/src/components/MainSidebar.vue @@ -99,7 +99,6 @@ import WorkflowSettings from '@/components/WorkflowSettings.vue'; import { genericHelpers } from '@/mixins/genericHelpers'; import { restApi } from '@/mixins/restApi'; import { showMessage } from '@/mixins/showMessage'; -import { titleChange } from '@/mixins/titleChange'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowRun } from '@/mixins/workflowRun'; @@ -115,13 +114,12 @@ import { useUsersStore } from '@/stores/users'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; import { useVersionsStore } from '@/stores/versions'; -import { isNavigationFailure, NavigationFailureType, Route } from 'vue-router'; +import { isNavigationFailure } from 'vue-router'; export default mixins( genericHelpers, restApi, showMessage, - titleChange, workflowHelpers, workflowRun, userHelpers, diff --git a/packages/editor-ui/src/composables/useTitleChange.ts b/packages/editor-ui/src/composables/useTitleChange.ts new file mode 100644 index 0000000000000..45ed1ab0139ee --- /dev/null +++ b/packages/editor-ui/src/composables/useTitleChange.ts @@ -0,0 +1,19 @@ +import { WorkflowTitleStatus } from '@/Interface'; + +export function useTitleChange() { + return { + titleSet(workflow: string, status: WorkflowTitleStatus) { + let icon = '⚠️'; + if (status === 'EXECUTING') { + icon = '🔄'; + } else if (status === 'IDLE') { + icon = '▶️'; + } + + window.document.title = `n8n - ${icon} ${workflow}`; + }, + titleReset() { + document.title = 'n8n - Workflow Automation'; + }, + }; +} diff --git a/packages/editor-ui/src/mixins/pushConnection.ts b/packages/editor-ui/src/mixins/pushConnection.ts index a43c5a5c35bc9..01b40ceaa99e9 100644 --- a/packages/editor-ui/src/mixins/pushConnection.ts +++ b/packages/editor-ui/src/mixins/pushConnection.ts @@ -8,7 +8,7 @@ import { import { externalHooks } from '@/mixins/externalHooks'; import { nodeHelpers } from '@/mixins/nodeHelpers'; import { showMessage } from '@/mixins/showMessage'; -import { titleChange } from '@/mixins/titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { @@ -39,9 +39,13 @@ export const pushConnection = mixins( externalHooks, nodeHelpers, showMessage, - titleChange, workflowHelpers, ).extend({ + setup() { + return { + ...useTitleChange(), + }; + }, data() { return { pushSource: null as WebSocket | EventSource | null, @@ -362,7 +366,7 @@ export const pushConnection = mixins( } // Workflow did start but had been put to wait - this.$titleSet(workflow.name as string, 'IDLE'); + this.titleSet(workflow.name as string, 'IDLE'); this.$showToast({ title: 'Workflow started waiting', message: `${action} More info`, @@ -370,7 +374,7 @@ export const pushConnection = mixins( duration: 0, }); } else if (runDataExecuted.finished !== true) { - this.$titleSet(workflow.name as string, 'ERROR'); + this.titleSet(workflow.name as string, 'ERROR'); if ( runDataExecuted.data.resultData.error?.name === 'ExpressionError' && @@ -441,7 +445,7 @@ export const pushConnection = mixins( } } else { // Workflow did execute without a problem - this.$titleSet(workflow.name as string, 'IDLE'); + this.titleSet(workflow.name as string, 'IDLE'); const execution = this.workflowsStore.getWorkflowExecution; if (execution && execution.executedNode) { diff --git a/packages/editor-ui/src/mixins/titleChange.ts b/packages/editor-ui/src/mixins/titleChange.ts deleted file mode 100644 index 63cde777c44e3..0000000000000 --- a/packages/editor-ui/src/mixins/titleChange.ts +++ /dev/null @@ -1,28 +0,0 @@ -import Vue from 'vue'; - -import { WorkflowTitleStatus } from '@/Interface'; - -export const titleChange = Vue.extend({ - methods: { - /** - * Change title of n8n tab - * - * @param {string} workflow Name of workflow - * @param {WorkflowTitleStatus} status Status of workflow - */ - $titleSet(workflow: string, status: WorkflowTitleStatus) { - let icon = '⚠️'; - if (status === 'EXECUTING') { - icon = '🔄'; - } else if (status === 'IDLE') { - icon = '▶️'; - } - - window.document.title = `n8n - ${icon} ${workflow}`; - }, - - $titleReset() { - document.title = 'n8n - Workflow Automation'; - }, - }, -}); diff --git a/packages/editor-ui/src/mixins/workflowRun.ts b/packages/editor-ui/src/mixins/workflowRun.ts index 80a5a137b058c..9b52780057418 100644 --- a/packages/editor-ui/src/mixins/workflowRun.ts +++ b/packages/editor-ui/src/mixins/workflowRun.ts @@ -14,19 +14,18 @@ import { workflowHelpers } from '@/mixins/workflowHelpers'; import { showMessage } from '@/mixins/showMessage'; import mixins from 'vue-typed-mixins'; -import { titleChange } from './titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import { mapStores } from 'pinia'; import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { useRootStore } from '@/stores/n8nRootStore'; -export const workflowRun = mixins( - externalHooks, - restApi, - workflowHelpers, - showMessage, - titleChange, -).extend({ +export const workflowRun = mixins(externalHooks, restApi, workflowHelpers, showMessage).extend({ + setup() { + return { + ...useTitleChange(), + }; + }, computed: { ...mapStores(useRootStore, useUIStore, useWorkflowsStore), }, @@ -72,7 +71,7 @@ export const workflowRun = mixins( return; } - this.$titleSet(workflow.name as string, 'EXECUTING'); + this.titleSet(workflow.name as string, 'EXECUTING'); this.clearAllStickyNotifications(); @@ -119,7 +118,7 @@ export const workflowRun = mixins( type: 'error', duration: 0, }); - this.$titleSet(workflow.name as string, 'ERROR'); + this.titleSet(workflow.name as string, 'ERROR'); this.$externalHooks().run('workflowRun.runError', { errorMessages, nodeName }); this.getWorkflowDataToSave().then((workflowData) => { @@ -245,7 +244,7 @@ export const workflowRun = mixins( return runWorkflowApiResponse; } catch (error) { - this.$titleSet(workflow.name as string, 'ERROR'); + this.titleSet(workflow.name as string, 'ERROR'); this.$showError(error, this.$locale.baseText('workflowRun.showError.title')); return undefined; } diff --git a/packages/editor-ui/src/views/NodeView.vue b/packages/editor-ui/src/views/NodeView.vue index e3b020bfe5dd0..2cc7730866105 100644 --- a/packages/editor-ui/src/views/NodeView.vue +++ b/packages/editor-ui/src/views/NodeView.vue @@ -209,7 +209,7 @@ import { restApi } from '@/mixins/restApi'; import useGlobalLinkActions from '@/composables/useGlobalLinkActions'; import useCanvasMouseSelect from '@/composables/useCanvasMouseSelect'; import { showMessage } from '@/mixins/showMessage'; -import { titleChange } from '@/mixins/titleChange'; +import { useTitleChange } from '@/composables/useTitleChange'; import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowRun } from '@/mixins/workflowRun'; @@ -226,6 +226,7 @@ import { IConnection, IConnections, IDataObject, + IExecutionsSummary, INode, INodeConnections, INodeCredentialsDetails, @@ -254,7 +255,6 @@ import type { ITag, INewWorkflowData, IWorkflowTemplate, - IExecutionsSummary, IWorkflowToShare, IUser, INodeUpdatePropertiesInformation, @@ -307,7 +307,6 @@ import { N8nPlusEndpointType, EVENT_PLUS_ENDPOINT_CLICK, } from '@/plugins/endpoints/N8nPlusEndpointType'; -import { usePostHog } from '@/stores/posthog'; interface AddNodeOptions { position?: XYPosition; @@ -325,7 +324,6 @@ export default mixins( moveNodeWorkflow, restApi, showMessage, - titleChange, workflowHelpers, workflowRun, debounceHelper, @@ -345,6 +343,7 @@ export default mixins( return { ...useCanvasMouseSelect(), ...useGlobalLinkActions(), + ...useTitleChange(), }; }, errorCaptured: (err, vm, info) => { @@ -1423,7 +1422,7 @@ export default mixins( this.workflowsStore.executingNode = null; this.uiStore.removeActiveAction('workflowRunning'); - this.$titleSet(this.workflowsStore.workflowName, 'IDLE'); + this.titleSet(this.workflowsStore.workflowName, 'IDLE'); this.$showMessage({ title: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.unsaved.title'), message: this.$locale.baseText( @@ -1447,7 +1446,7 @@ export default mixins( retryOf: execution.retryOf, } as IPushDataExecutionFinished; this.workflowsStore.finishActiveExecution(pushData); - this.$titleSet(execution.workflowData.name, 'IDLE'); + this.titleSet(execution.workflowData.name, 'IDLE'); this.workflowsStore.executingNode = null; this.workflowsStore.setWorkflowExecutionData(executedData as IExecutionResponse); this.uiStore.removeActiveAction('workflowRunning'); @@ -2597,7 +2596,7 @@ export default mixins( } if (workflow) { - this.$titleSet(workflow.name, 'IDLE'); + this.titleSet(workflow.name, 'IDLE'); // Open existing workflow await this.openWorkflow(workflow); } @@ -3848,7 +3847,7 @@ export default mixins( async mounted() { this.resetWorkspace(); this.canvasStore.initInstance(this.$refs.nodeView as HTMLElement); - this.$titleReset(); + this.titleReset(); window.addEventListener('message', this.onPostMessageReceived); this.startLoading(); From 54f99a7d0d9adafc9e7433b38eadd5f93b34d80b Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 21 Apr 2023 16:59:04 +0300 Subject: [PATCH 011/110] feat: Replace this.$refs.refName as Vue with InstanceType (no-changelog) (#6050) * refactor: use InstanceType for all this.$refs types * refactor: update refs type in N8nSelect * fix: remove inputRef non-null assertion Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> * fix: remove non-null assertion --------- Co-authored-by: Mutasem Aldmour <4711238+mutdmour@users.noreply.github.com> --- .../N8nButton/overrides/ElButton.vue | 4 +- .../src/components/N8nInput/Input.vue | 8 +- .../src/components/N8nSelect/Select.vue | 24 ++-- .../design-system/src/components/index.ts | 49 ++++++++ packages/design-system/src/main.ts | 5 +- packages/design-system/src/plugin.ts | 107 ++++++++++++++++++ .../src/plugins/n8nComponents.ts | 106 ----------------- .../CodeNodeEditor/CodeNodeEditor.vue | 8 +- .../src/components/CollectionsCarousel.vue | 17 +-- .../CredentialEdit/CredentialEdit.vue | 12 +- .../src/components/CredentialsSelect.vue | 9 +- .../src/components/CredentialsSelectModal.vue | 6 +- .../src/components/DraggableTarget.vue | 6 +- .../components/DuplicateWorkflowDialog.vue | 6 +- .../ExecutionsView/ExecutionPreview.vue | 8 +- .../ExecutionsView/ExecutionsSidebar.vue | 37 +++--- .../components/ExpressionParameterInput.vue | 9 +- .../src/components/HtmlEditor/HtmlEditor.vue | 9 +- .../src/components/InlineNameEdit.vue | 6 +- .../components/MainHeader/WorkflowDetails.vue | 6 +- .../editor-ui/src/components/NodeTitle.vue | 6 +- .../editor-ui/src/components/OutputPanel.vue | 7 +- .../src/components/ParameterInput.vue | 9 +- .../src/components/ParameterInputExpanded.vue | 4 +- .../src/components/ParameterInputFull.vue | 11 +- .../src/components/ParameterInputWrapper.vue | 8 +- .../ResourceLocator/ResourceLocator.vue | 16 +-- .../ResourceLocatorDropdown.vue | 34 +++--- packages/editor-ui/src/components/RunData.vue | 6 +- .../editor-ui/src/components/RunDataTable.vue | 4 +- .../editor-ui/src/components/TagsDropdown.vue | 52 ++++----- .../TagsManager/TagsView/TagsTable.vue | 25 ++-- .../editor-ui/src/components/TemplateList.vue | 6 +- .../editor-ui/src/components/TriggerPanel.vue | 5 +- .../editor-ui/src/components/WorkflowCard.vue | 4 +- .../src/components/WorkflowPreview.vue | 12 +- .../layouts/ResourcesListLayout.vue | 6 +- packages/editor-ui/src/mixins/emitter.ts | 30 +++-- packages/editor-ui/src/plugins/components.ts | 6 +- packages/editor-ui/src/views/NodeView.vue | 6 +- .../editor-ui/src/views/SettingsLdapView.vue | 46 ++++---- 41 files changed, 427 insertions(+), 318 deletions(-) create mode 100644 packages/design-system/src/components/index.ts create mode 100644 packages/design-system/src/plugin.ts delete mode 100644 packages/design-system/src/plugins/n8nComponents.ts diff --git a/packages/design-system/src/components/N8nButton/overrides/ElButton.vue b/packages/design-system/src/components/N8nButton/overrides/ElButton.vue index 817e6d7a5566a..1cbb31b8200f3 100644 --- a/packages/design-system/src/components/N8nButton/overrides/ElButton.vue +++ b/packages/design-system/src/components/N8nButton/overrides/ElButton.vue @@ -13,6 +13,8 @@ const classToTypeMap = { 'el-picker-panel__link-btn': 'secondary', }; +type ButtonRef = InstanceType; + export default defineComponent({ components: { N8nButton, @@ -32,7 +34,7 @@ export default defineComponent({ } Object.entries(classToTypeMap).forEach(([className, mappedType]) => { - if (this.$refs.button && (this.$refs.button as Vue).$el.classList.contains(className)) { + if ((this.$refs.button as ButtonRef)?.$el.classList.contains(className)) { type = mappedType; } }); diff --git a/packages/design-system/src/components/N8nInput/Input.vue b/packages/design-system/src/components/N8nInput/Input.vue index 3408ea14ae9ed..68e617d590f16 100644 --- a/packages/design-system/src/components/N8nInput/Input.vue +++ b/packages/design-system/src/components/N8nInput/Input.vue @@ -27,6 +27,8 @@ import { Input as ElInput } from 'element-ui'; import { defineComponent } from 'vue'; +type InputRef = InstanceType; + export default defineComponent({ name: 'n8n-input', components: { @@ -92,7 +94,7 @@ export default defineComponent({ }, methods: { focus() { - const innerInput = this.$refs.innerInput as Vue | undefined; + const innerInput = this.$refs.innerInput as InputRef | undefined; if (!innerInput) return; @@ -105,7 +107,7 @@ export default defineComponent({ inputElement.focus(); }, blur() { - const innerInput = this.$refs.innerInput as Vue | undefined; + const innerInput = this.$refs.innerInput as InputRef | undefined; if (!innerInput) return; @@ -118,7 +120,7 @@ export default defineComponent({ inputElement.blur(); }, select() { - const innerInput = this.$refs.innerInput as Vue | undefined; + const innerInput = this.$refs.innerInput as InputRef | undefined; if (!innerInput) return; diff --git a/packages/design-system/src/components/N8nSelect/Select.vue b/packages/design-system/src/components/N8nSelect/Select.vue index 856c4a9d986b9..7ea6eeb799c71 100644 --- a/packages/design-system/src/components/N8nSelect/Select.vue +++ b/packages/design-system/src/components/N8nSelect/Select.vue @@ -35,6 +35,8 @@ import { Select as ElSelect } from 'element-ui'; import { defineComponent } from 'vue'; +type InnerSelectRef = InstanceType; + export interface IProps { size?: string; limitPopperWidth?: string; @@ -117,23 +119,23 @@ export default defineComponent({ }, methods: { focus() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - select.focus(); + const selectRef = this.$refs.innerSelect as InnerSelectRef | undefined; + if (selectRef) { + selectRef.focus(); } }, blur() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - select.blur(); + const selectRef = this.$refs.innerSelect as InnerSelectRef | undefined; + if (selectRef) { + selectRef.blur(); } }, focusOnInput() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - const input = select.$refs.input as (Vue & HTMLElement) | undefined; - if (input) { - input.focus(); + const selectRef = this.$refs.innerSelect as InnerSelectRef | undefined; + if (selectRef) { + const inputRef = selectRef.$refs.input as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } } }, diff --git a/packages/design-system/src/components/index.ts b/packages/design-system/src/components/index.ts new file mode 100644 index 0000000000000..22683c0296de4 --- /dev/null +++ b/packages/design-system/src/components/index.ts @@ -0,0 +1,49 @@ +export { default as N8nActionBox } from './N8nActionBox'; +export { default as N8nActionDropdown } from './N8nActionDropdown'; +export { default as N8nActionToggle } from './N8nActionToggle'; +export { default as N8nAlert } from './N8nAlert'; +export { default as N8nAvatar } from './N8nAvatar'; +export { default as N8nBadge } from './N8nBadge'; +export { default as N8nBlockUi } from './N8nBlockUi'; +export { default as N8nButton } from './N8nButton'; +export { N8nElButton } from './N8nButton/overrides'; +export { default as N8nCallout } from './N8nCallout'; +export { default as N8nCard } from './N8nCard'; +export { default as N8nDatatable } from './N8nDatatable'; +export { default as N8nFormBox } from './N8nFormBox'; +export { default as N8nFormInputs } from './N8nFormInputs'; +export { default as N8nFormInput } from './N8nFormInput'; +export { default as N8nHeading } from './N8nHeading'; +export { default as N8nIcon } from './N8nIcon'; +export { default as N8nIconButton } from './N8nIconButton'; +export { default as N8nInfoAccordion } from './N8nInfoAccordion'; +export { default as N8nInfoTip } from './N8nInfoTip'; +export { default as N8nInput } from './N8nInput'; +export { default as N8nInputLabel } from './N8nInputLabel'; +export { default as N8nInputNumber } from './N8nInputNumber'; +export { default as N8nLink } from './N8nLink'; +export { default as N8nLoading } from './N8nLoading'; +export { default as N8nMarkdown } from './N8nMarkdown'; +export { default as N8nMenu } from './N8nMenu'; +export { default as N8nMenuItem } from './N8nMenuItem'; +export { default as N8nNodeCreatorNode } from './N8nNodeCreatorNode'; +export { default as N8nNodeIcon } from './N8nNodeIcon'; +export { default as N8nNotice } from './N8nNotice'; +export { default as N8nOption } from './N8nOption'; +export { default as N8nPopover } from './N8nPopover'; +export { default as N8nPulse } from './N8nPulse'; +export { default as N8nRadioButtons } from './N8nRadioButtons'; +export { default as N8nSelect } from './N8nSelect'; +export { default as N8nSpinner } from './N8nSpinner'; +export { default as N8nSticky } from './N8nSticky'; +export { default as N8nTabs } from './N8nTabs'; +export { default as N8nTag } from './N8nTag'; +export { default as N8nTags } from './N8nTags'; +export { default as N8nText } from './N8nText'; +export { default as N8nTooltip } from './N8nTooltip'; +export { default as N8nTree } from './N8nTree'; +export { default as N8nUserInfo } from './N8nUserInfo'; +export { default as N8nUserSelect } from './N8nUserSelect'; +export { default as N8nUsersList } from './N8nUsersList'; +export { default as N8nResizeWrapper } from './N8nResizeWrapper'; +export { default as N8nRecycleScroller } from './N8nRecycleScroller'; diff --git a/packages/design-system/src/main.ts b/packages/design-system/src/main.ts index 65ae159c2ca85..cf75163855806 100644 --- a/packages/design-system/src/main.ts +++ b/packages/design-system/src/main.ts @@ -1,6 +1,7 @@ import * as locale from './locale'; -import designSystemComponents from './plugins/n8nComponents'; +export * from './components'; +export * from './plugin'; export * from './types'; export * from './utils'; -export { locale, designSystemComponents }; +export { locale }; diff --git a/packages/design-system/src/plugin.ts b/packages/design-system/src/plugin.ts new file mode 100644 index 0000000000000..0a9a47021014b --- /dev/null +++ b/packages/design-system/src/plugin.ts @@ -0,0 +1,107 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { PluginObject } from 'vue'; +import { + N8nActionBox, + N8nActionDropdown, + N8nActionToggle, + N8nAlert, + N8nAvatar, + N8nBadge, + N8nBlockUi, + N8nButton, + N8nElButton, + N8nCallout, + N8nCard, + N8nDatatable, + N8nFormBox, + N8nFormInputs, + N8nFormInput, + N8nHeading, + N8nIcon, + N8nIconButton, + N8nInfoAccordion, + N8nInfoTip, + N8nInput, + N8nInputLabel, + N8nInputNumber, + N8nLink, + N8nLoading, + N8nMarkdown, + N8nMenu, + N8nMenuItem, + N8nNodeCreatorNode, + N8nNodeIcon, + N8nNotice, + N8nOption, + N8nPopover, + N8nPulse, + N8nRadioButtons, + N8nSelect, + N8nSpinner, + N8nSticky, + N8nTabs, + N8nTag, + N8nTags, + N8nText, + N8nTooltip, + N8nTree, + N8nUserInfo, + N8nUserSelect, + N8nUsersList, + N8nResizeWrapper, + N8nRecycleScroller, +} from './components'; + +export const N8nPlugin: PluginObject<{}> = { + install: (app) => { + app.component('n8n-info-accordion', N8nInfoAccordion); + app.component('n8n-action-box', N8nActionBox); + app.component('n8n-action-dropdown', N8nActionDropdown); + app.component('n8n-action-toggle', N8nActionToggle); + app.component('n8n-alert', N8nAlert); + app.component('n8n-avatar', N8nAvatar); + app.component('n8n-badge', N8nBadge); + app.component('n8n-block-ui', N8nBlockUi); + app.component('n8n-button', N8nButton); + app.component('el-button', N8nElButton); + app.component('n8n-callout', N8nCallout); + app.component('n8n-card', N8nCard); + app.component('n8n-datatable', N8nDatatable); + app.component('n8n-form-box', N8nFormBox); + app.component('n8n-form-inputs', N8nFormInputs); + app.component('n8n-form-input', N8nFormInput); + app.component('n8n-icon', N8nIcon); + app.component('n8n-icon-button', N8nIconButton); + app.component('n8n-info-tip', N8nInfoTip); + app.component('n8n-input', N8nInput); + app.component('n8n-input-label', N8nInputLabel); + app.component('n8n-input-number', N8nInputNumber); + app.component('n8n-loading', N8nLoading); + app.component('n8n-heading', N8nHeading); + app.component('n8n-link', N8nLink); + app.component('n8n-markdown', N8nMarkdown); + app.component('n8n-menu', N8nMenu); + app.component('n8n-menu-item', N8nMenuItem); + app.component('n8n-node-creator-node', N8nNodeCreatorNode); + app.component('n8n-node-icon', N8nNodeIcon); + app.component('n8n-notice', N8nNotice); + app.component('n8n-option', N8nOption); + app.component('n8n-popover', N8nPopover); + app.component('n8n-pulse', N8nPulse); + app.component('n8n-select', N8nSelect); + app.component('n8n-spinner', N8nSpinner); + app.component('n8n-sticky', N8nSticky); + app.component('n8n-radio-buttons', N8nRadioButtons); + app.component('n8n-tags', N8nTags); + app.component('n8n-tabs', N8nTabs); + app.component('n8n-tag', N8nTag); + app.component('n8n-text', N8nText); + app.component('n8n-tooltip', N8nTooltip); + app.component('n8n-user-info', N8nUserInfo); + app.component('n8n-tree', N8nTree); + app.component('n8n-users-list', N8nUsersList); + app.component('n8n-user-select', N8nUserSelect); + app.component('n8n-resize-wrapper', N8nResizeWrapper); + app.component('n8n-recycle-scroller', N8nRecycleScroller); + }, +}; diff --git a/packages/design-system/src/plugins/n8nComponents.ts b/packages/design-system/src/plugins/n8nComponents.ts deleted file mode 100644 index 6c182b53f2cba..0000000000000 --- a/packages/design-system/src/plugins/n8nComponents.ts +++ /dev/null @@ -1,106 +0,0 @@ -import type { PluginObject } from 'vue'; -import N8nActionBox from '../components/N8nActionBox'; -import N8nActionDropdown from '../components/N8nActionDropdown'; -import N8nActionToggle from '../components/N8nActionToggle'; -import N8nAlert from '../components/N8nAlert'; -import N8nAvatar from '../components/N8nAvatar'; -import N8nBadge from '../components/N8nBadge'; -import N8nBlockUi from '../components/N8nBlockUi'; -import N8nButton from '../components/N8nButton'; -import { N8nElButton } from '../components/N8nButton/overrides'; -import N8nCallout from '../components/N8nCallout'; -import N8nCard from '../components/N8nCard'; -import N8nDatatable from '../components/N8nDatatable'; -import N8nFormBox from '../components/N8nFormBox'; -import N8nFormInputs from '../components/N8nFormInputs'; -import N8nFormInput from '../components/N8nFormInput'; -import N8nHeading from '../components/N8nHeading'; -import N8nIcon from '../components/N8nIcon'; -import N8nIconButton from '../components/N8nIconButton'; -import N8nInfoAccordion from '../components/N8nInfoAccordion'; -import N8nInfoTip from '../components/N8nInfoTip'; -import { default as N8nInput } from '../components/N8nInput'; -import N8nInputLabel from '../components/N8nInputLabel'; -import N8nInputNumber from '../components/N8nInputNumber'; -import N8nLink from '../components/N8nLink'; -import N8nLoading from '../components/N8nLoading'; -import N8nMarkdown from '../components/N8nMarkdown'; -import N8nMenu from '../components/N8nMenu'; -import N8nMenuItem from '../components/N8nMenuItem'; -import N8nNodeCreatorNode from '../components/N8nNodeCreatorNode'; -import N8nNodeIcon from '../components/N8nNodeIcon'; -import N8nNotice from '../components/N8nNotice'; -import N8nOption from '../components/N8nOption'; -import N8nPopover from '../components/N8nPopover'; -import N8nPulse from '../components/N8nPulse'; -import N8nRadioButtons from '../components/N8nRadioButtons'; -import N8nSelect from '../components/N8nSelect'; -import N8nSpinner from '../components/N8nSpinner'; -import N8nSticky from '../components/N8nSticky'; -import N8nTabs from '../components/N8nTabs'; -import N8nTag from '../components/N8nTag'; -import N8nTags from '../components/N8nTags'; -import N8nText from '../components/N8nText'; -import N8nTooltip from '../components/N8nTooltip'; -import N8nTree from '../components/N8nTree'; -import N8nUserInfo from '../components/N8nUserInfo'; -import N8nUserSelect from '../components/N8nUserSelect'; -import N8nUsersList from '../components/N8nUsersList'; -import N8nResizeWrapper from '../components/N8nResizeWrapper'; -import N8nRecycleScroller from '../components/N8nRecycleScroller'; - -const n8nComponentsPlugin: PluginObject<{}> = { - install: (app) => { - app.component('n8n-info-accordion', N8nInfoAccordion); - app.component('n8n-action-box', N8nActionBox); - app.component('n8n-action-dropdown', N8nActionDropdown); - app.component('n8n-action-toggle', N8nActionToggle); - app.component('n8n-alert', N8nAlert); - app.component('n8n-avatar', N8nAvatar); - app.component('n8n-badge', N8nBadge); - app.component('n8n-block-ui', N8nBlockUi); - app.component('n8n-button', N8nButton); - app.component('el-button', N8nElButton); - app.component('n8n-callout', N8nCallout); - app.component('n8n-card', N8nCard); - app.component('n8n-datatable', N8nDatatable); - app.component('n8n-form-box', N8nFormBox); - app.component('n8n-form-inputs', N8nFormInputs); - app.component('n8n-form-input', N8nFormInput); - app.component('n8n-icon', N8nIcon); - app.component('n8n-icon-button', N8nIconButton); - app.component('n8n-info-tip', N8nInfoTip); - app.component('n8n-input', N8nInput); - app.component('n8n-input-label', N8nInputLabel); - app.component('n8n-input-number', N8nInputNumber); - app.component('n8n-loading', N8nLoading); - app.component('n8n-heading', N8nHeading); - app.component('n8n-link', N8nLink); - app.component('n8n-markdown', N8nMarkdown); - app.component('n8n-menu', N8nMenu); - app.component('n8n-menu-item', N8nMenuItem); - app.component('n8n-node-creator-node', N8nNodeCreatorNode); - app.component('n8n-node-icon', N8nNodeIcon); - app.component('n8n-notice', N8nNotice); - app.component('n8n-option', N8nOption); - app.component('n8n-popover', N8nPopover); - app.component('n8n-pulse', N8nPulse); - app.component('n8n-select', N8nSelect); - app.component('n8n-spinner', N8nSpinner); - app.component('n8n-sticky', N8nSticky); - app.component('n8n-radio-buttons', N8nRadioButtons); - app.component('n8n-tags', N8nTags); - app.component('n8n-tabs', N8nTabs); - app.component('n8n-tag', N8nTag); - app.component('n8n-text', N8nText); - app.component('n8n-tooltip', N8nTooltip); - app.component('n8n-user-info', N8nUserInfo); - app.component('n8n-tree', N8nTree); - app.component('n8n-users-list', N8nUsersList); - app.component('n8n-user-select', N8nUserSelect); - app.component('n8n-resize-wrapper', N8nResizeWrapper); - app.component('n8n-recycle-scroller', N8nRecycleScroller); - }, -}; - -export default n8nComponentsPlugin; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index fff8c461fa446..3c95a9d01817d 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -95,15 +95,15 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte methods: { onMouseOver(event: MouseEvent) { const fromElement = event.relatedTarget as HTMLElement; - const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement; + const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined; - if (!ref.contains(fromElement)) this.isEditorHovered = true; + if (!ref?.contains(fromElement)) this.isEditorHovered = true; }, onMouseOut(event: MouseEvent) { const fromElement = event.relatedTarget as HTMLElement; - const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement; + const ref = this.$refs.codeNodeEditorContainer as HTMLDivElement | undefined; - if (!ref.contains(fromElement)) this.isEditorHovered = false; + if (!ref?.contains(fromElement)) this.isEditorHovered = false; }, onAskAiButtonClick() { this.$telemetry.track('User clicked ask ai button', { source: 'code' }); diff --git a/packages/editor-ui/src/components/CollectionsCarousel.vue b/packages/editor-ui/src/components/CollectionsCarousel.vue index 00505c2781c65..cc86203f2dfd1 100644 --- a/packages/editor-ui/src/components/CollectionsCarousel.vue +++ b/packages/editor-ui/src/components/CollectionsCarousel.vue @@ -35,6 +35,8 @@ import VueAgile from 'vue-agile'; import { genericHelpers } from '@/mixins/genericHelpers'; import mixins from 'vue-typed-mixins'; +type SliderRef = InstanceType; + export default mixins(genericHelpers).extend({ name: 'CollectionsCarousel', props: { @@ -97,22 +99,23 @@ export default mixins(genericHelpers).extend({ }, mounted() { this.$nextTick(() => { - const slider = this.$refs.slider; - if (!slider) { + const sliderRef = this.$refs.slider as SliderRef | undefined; + if (!sliderRef) { return; } - // @ts-ignore - this.listElement = slider.$el.querySelector('.agile__list'); + + this.listElement = sliderRef.$el.querySelector('.agile__list'); if (this.listElement) { this.listElement.addEventListener('scroll', this.updateCarouselScroll); } }); }, beforeDestroy() { - if (this.$refs.slider) { - // @ts-ignore - this.$refs.slider.destroy(); + const sliderRef = this.$refs.slider as SliderRef | undefined; + if (sliderRef) { + sliderRef.destroy(); } + window.removeEventListener('scroll', this.updateCarouselScroll); }, }); diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 29b970f90cf90..5f37e2b0bdc10 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -672,18 +672,18 @@ export default mixins(showMessage, nodeHelpers).extend({ scrollToTop() { setTimeout(() => { - const content = this.$refs.content as Element; - if (content) { - content.scrollTop = 0; + const contentRef = this.$refs.content as Element | undefined; + if (contentRef) { + contentRef.scrollTop = 0; } }, 0); }, scrollToBottom() { setTimeout(() => { - const content = this.$refs.content as Element; - if (content) { - content.scrollTop = content.scrollHeight; + const contentRef = this.$refs.content as Element | undefined; + if (contentRef) { + contentRef.scrollTop = contentRef.scrollHeight; } }, 0); }, diff --git a/packages/editor-ui/src/components/CredentialsSelect.vue b/packages/editor-ui/src/components/CredentialsSelect.vue index 559bed74dd1ae..d24ff30306436 100644 --- a/packages/editor-ui/src/components/CredentialsSelect.vue +++ b/packages/editor-ui/src/components/CredentialsSelect.vue @@ -61,6 +61,9 @@ import ScopesNotice from '@/components/ScopesNotice.vue'; import NodeCredentials from '@/components/NodeCredentials.vue'; import { mapStores } from 'pinia'; import { useCredentialsStore } from '@/stores/credentials'; +import { N8nSelect } from 'n8n-design-system'; + +type N8nSelectRef = InstanceType; export default Vue.extend({ name: 'CredentialsSelect', @@ -93,9 +96,9 @@ export default Vue.extend({ }, methods: { focus() { - const select = this.$refs.innerSelect as (Vue & HTMLElement) | undefined; - if (select) { - select.focus(); + const selectRef = this.$refs.innerSelect as N8nSelectRef | undefined; + if (selectRef) { + selectRef.focus(); } }, /** diff --git a/packages/editor-ui/src/components/CredentialsSelectModal.vue b/packages/editor-ui/src/components/CredentialsSelectModal.vue index b2470bd0f6e7f..8298633f39f79 100644 --- a/packages/editor-ui/src/components/CredentialsSelectModal.vue +++ b/packages/editor-ui/src/components/CredentialsSelectModal.vue @@ -80,9 +80,9 @@ export default mixins(externalHooks).extend({ this.loading = false; setTimeout(() => { - const element = this.$refs.select as HTMLSelectElement; - if (element) { - element.focus(); + const elementRef = this.$refs.select as HTMLSelectElement | undefined; + if (elementRef) { + elementRef.focus(); } }, 0); }, diff --git a/packages/editor-ui/src/components/DraggableTarget.vue b/packages/editor-ui/src/components/DraggableTarget.vue index 742ff4d515cf4..5ee76876c382c 100644 --- a/packages/editor-ui/src/components/DraggableTarget.vue +++ b/packages/editor-ui/src/components/DraggableTarget.vue @@ -55,10 +55,10 @@ export default Vue.extend({ }, methods: { onMouseMove(e: MouseEvent) { - const target = this.$refs.target as HTMLElement; + const targetRef = this.$refs.target as HTMLElement | undefined; - if (target && this.isDragging) { - const dim = target.getBoundingClientRect(); + if (targetRef && this.isDragging) { + const dim = targetRef.getBoundingClientRect(); this.hovering = e.clientX >= dim.left && diff --git a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue index 3628ee0c95e55..fa433c420a2d7 100644 --- a/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue +++ b/packages/editor-ui/src/components/DuplicateWorkflowDialog.vue @@ -115,9 +115,9 @@ export default mixins(showMessage, workflowHelpers, restApi).extend({ this.dropdownBus.emit('focus'); }, focusOnNameInput() { - const input = this.$refs.nameInput as HTMLElement; - if (input && input.focus) { - input.focus(); + const inputRef = this.$refs.nameInput as HTMLElement | undefined; + if (inputRef && inputRef.focus) { + inputRef.focus(); } }, onTagsBlur() { diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue index bd5774a47ad3c..1df971aac6974 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionPreview.vue @@ -137,6 +137,8 @@ import { useUIStore } from '@/stores/ui'; import { Dropdown as ElDropdown } from 'element-ui'; import { IAbstractEventMessage } from 'n8n-workflow'; +type RetryDropdownRef = InstanceType & { hide: () => void }; + export default mixins(restApi, showMessage, executionHelpers).extend({ name: 'execution-preview', components: { @@ -182,9 +184,9 @@ export default mixins(restApi, showMessage, executionHelpers).extend({ }, onRetryButtonBlur(event: FocusEvent): void { // Hide dropdown when clicking outside of current document - const retryDropdown = this.$refs.retryDropdown as (Vue & { hide: () => void }) | undefined; - if (retryDropdown && event.relatedTarget === null) { - retryDropdown.hide(); + const retryDropdownRef = this.$refs.retryDropdown as RetryDropdownRef | undefined; + if (retryDropdownRef && event.relatedTarget === null) { + retryDropdownRef.hide(); } }, }, diff --git a/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue b/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue index 4a126a33d37da..1f8e7d3c2d00c 100644 --- a/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue +++ b/packages/editor-ui/src/components/ExecutionsView/ExecutionsSidebar.vue @@ -77,6 +77,8 @@ import { useUIStore } from '@/stores/ui'; import { useWorkflowsStore } from '@/stores/workflows'; import { ExecutionFilterType } from '@/Interface'; +type ExecutionCardRef = InstanceType; + export default Vue.extend({ name: 'executions-sidebar', components: { @@ -144,10 +146,11 @@ export default Vue.extend({ methods: { loadMore(limit = 20): void { if (!this.loading) { - const executionsList = this.$refs.executionList as HTMLElement; - if (executionsList) { + const executionsListRef = this.$refs.executionList as HTMLElement | undefined; + if (executionsListRef) { const diff = - executionsList.offsetHeight - (executionsList.scrollHeight - executionsList.scrollTop); + executionsListRef.offsetHeight - + (executionsListRef.scrollHeight - executionsListRef.scrollTop); if (diff > -10 && diff < 10) { this.$emit('loadMore', limit); } @@ -178,16 +181,16 @@ export default Vue.extend({ } }, checkListSize(): void { - const sidebarContainer = this.$refs.container as HTMLElement; - const currentExecutionCard = this.$refs[ + const sidebarContainerRef = this.$refs.container as HTMLElement | undefined; + const currentExecutionCardRefs = this.$refs[ `execution-${this.workflowsStore.activeWorkflowExecution?.id}` - ] as Vue[]; + ] as ExecutionCardRef[] | undefined; // Find out how many execution card can fit into list // and load more if needed - if (sidebarContainer && currentExecutionCard?.length) { - const cardElement = currentExecutionCard[0].$el as HTMLElement; - const listCapacity = Math.ceil(sidebarContainer.clientHeight / cardElement.clientHeight); + if (sidebarContainerRef && currentExecutionCardRefs?.length) { + const cardElement = currentExecutionCardRefs[0].$el as HTMLElement; + const listCapacity = Math.ceil(sidebarContainerRef.clientHeight / cardElement.clientHeight); if (listCapacity > this.executions.length) { this.$emit('loadMore', listCapacity - this.executions.length); @@ -195,21 +198,21 @@ export default Vue.extend({ } }, scrollToActiveCard(): void { - const executionsList = this.$refs.executionList as HTMLElement; - const currentExecutionCard = this.$refs[ + const executionsListRef = this.$refs.executionList as HTMLElement | undefined; + const currentExecutionCardRefs = this.$refs[ `execution-${this.workflowsStore.activeWorkflowExecution?.id}` - ] as Vue[]; + ] as ExecutionCardRef[] | undefined; if ( - executionsList && - currentExecutionCard?.length && + executionsListRef && + currentExecutionCardRefs?.length && this.workflowsStore.activeWorkflowExecution ) { - const cardElement = currentExecutionCard[0].$el as HTMLElement; + const cardElement = currentExecutionCardRefs[0].$el as HTMLElement; const cardRect = cardElement.getBoundingClientRect(); const LIST_HEADER_OFFSET = 200; - if (cardRect.top > executionsList.offsetHeight) { - executionsList.scrollTo({ top: cardRect.top - LIST_HEADER_OFFSET }); + if (cardRect.top > executionsListRef.offsetHeight) { + executionsListRef.scrollTo({ top: cardRect.top - LIST_HEADER_OFFSET }); } } }, diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue index 3a8b6e79a5804..4ef4f0519edc4 100644 --- a/packages/editor-ui/src/components/ExpressionParameterInput.vue +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -79,6 +79,8 @@ import { EXPRESSIONS_DOCS_URL } from '@/constants'; import type { Segment } from '@/types/expressions'; import type { TargetItem } from '@/Interface'; +type InlineExpressionEditorInputRef = InstanceType; + export default Vue.extend({ name: 'ExpressionParameterInput', components: { @@ -127,9 +129,10 @@ export default Vue.extend({ }, methods: { focus() { - const inlineInput = this.$refs.inlineInput as (Vue & HTMLElement) | undefined; - - if (inlineInput?.$el) inlineInput.focus(); + const inlineInputRef = this.$refs.inlineInput as InlineExpressionEditorInputRef | undefined; + if (inlineInputRef?.$el) { + inlineInputRef.focus(); + } }, onFocus() { this.isFocused = true; diff --git a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue index 23b7168b962d3..133c9be7514e0 100644 --- a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue +++ b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue @@ -169,11 +169,12 @@ export default mixins(expressionManager).extend({ methods: { root() { - const root = this.$refs.htmlEditor as HTMLDivElement | undefined; - - if (!root) throw new Error('Expected div with ref "htmlEditor"'); + const rootRef = this.$refs.htmlEditor as HTMLDivElement | undefined; + if (!rootRef) { + throw new Error('Expected div with ref "htmlEditor"'); + } - return root; + return rootRef; }, isMissingHtmlTags() { diff --git a/packages/editor-ui/src/components/InlineNameEdit.vue b/packages/editor-ui/src/components/InlineNameEdit.vue index 4ec839fbd3cf0..269c7d865659a 100644 --- a/packages/editor-ui/src/components/InlineNameEdit.vue +++ b/packages/editor-ui/src/components/InlineNameEdit.vue @@ -65,9 +65,9 @@ export default mixins(showMessage).extend({ this.isNameEdit = true; setTimeout(() => { - const input = this.$refs.nameInput as HTMLInputElement; - if (input) { - input.focus(); + const inputRef = this.$refs.nameInput as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } }, 0); }, diff --git a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue index 44a5027f63687..0e194b87df2ec 100644 --- a/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue +++ b/packages/editor-ui/src/components/MainHeader/WorkflowDetails.vue @@ -424,9 +424,9 @@ export default mixins(workflowHelpers).extend({ this.$root.$emit('importWorkflowData', { data: workflowData }); }; - const input = this.$refs.importFile as HTMLInputElement; - if (input !== null && input.files !== null && input.files.length !== 0) { - reader.readAsText(input!.files[0]!); + const inputRef = this.$refs.importFile as HTMLInputElement | undefined; + if (inputRef?.files && inputRef.files.length !== 0) { + reader.readAsText(inputRef.files[0]); } }, async onWorkflowMenuSelect(action: string): Promise { diff --git a/packages/editor-ui/src/components/NodeTitle.vue b/packages/editor-ui/src/components/NodeTitle.vue index a5a045b6be5e2..2e51615c7b377 100644 --- a/packages/editor-ui/src/components/NodeTitle.vue +++ b/packages/editor-ui/src/components/NodeTitle.vue @@ -70,9 +70,9 @@ export default Vue.extend({ this.newName = this.value; this.editName = true; this.$nextTick(() => { - const input = this.$refs.input; - if (input) { - (input as HTMLInputElement).focus(); + const inputRef = this.$refs.input as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } }); }, diff --git a/packages/editor-ui/src/components/OutputPanel.vue b/packages/editor-ui/src/components/OutputPanel.vue index 3810071352a47..5c764637c0ddd 100644 --- a/packages/editor-ui/src/components/OutputPanel.vue +++ b/packages/editor-ui/src/components/OutputPanel.vue @@ -111,7 +111,7 @@ import { useWorkflowsStore } from '@/stores/workflows'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; -type RunDataRef = Vue & { enterEditMode: (args: EnterEditModeArgs) => void }; +type RunDataRef = InstanceType; export default mixins(pinData).extend({ name: 'OutputPanel', @@ -242,8 +242,9 @@ export default mixins(pinData).extend({ }, methods: { insertTestData() { - if (this.$refs.runData) { - (this.$refs.runData as RunDataRef).enterEditMode({ + const runDataRef = this.$refs.runData as RunDataRef | undefined; + if (runDataRef) { + runDataRef.enterEditMode({ origin: 'insertTestDataLink', }); diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 0cd176a07c852..5936310e3178b 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -375,6 +375,8 @@ import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useCredentialsStore } from '@/stores/credentials'; import { htmlEditorEventBus } from '@/event-bus'; +type ResourceLocatorRef = InstanceType; + export default mixins( externalHooks, nodeHelpers, @@ -1099,10 +1101,9 @@ export default mixins( } } else if (command === 'refreshOptions') { if (this.isResourceLocatorParameter) { - const resourceLocator = this.$refs.resourceLocator; - if (resourceLocator) { - (resourceLocator as Vue).$emit('refreshList'); - } + const resourceLocatorRef = this.$refs.resourceLocator as ResourceLocatorRef | undefined; + + resourceLocatorRef?.$emit('refreshList'); } this.loadRemoteParameterOptions(); } else if (command === 'formatHtml') { diff --git a/packages/editor-ui/src/components/ParameterInputExpanded.vue b/packages/editor-ui/src/components/ParameterInputExpanded.vue index bba92cbbfde30..53dc51d41227e 100644 --- a/packages/editor-ui/src/components/ParameterInputExpanded.vue +++ b/packages/editor-ui/src/components/ParameterInputExpanded.vue @@ -65,6 +65,8 @@ import { INodeParameterResourceLocator, INodeProperties, IParameterLabel } from import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; +type ParamRef = InstanceType; + export default Vue.extend({ name: 'parameter-input-expanded', components: { @@ -145,7 +147,7 @@ export default Vue.extend({ }, optionSelected(command: string) { if (this.$refs.param) { - (this.$refs.param as Vue).$emit('optionSelected', command); + (this.$refs.param as ParamRef).$emit('optionSelected', command); } }, valueChanged(parameterData: IUpdateInformation) { diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index b884d76b12808..35a15f7d2c866 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -92,6 +92,8 @@ import { useSegment } from '@/stores/segment'; import { externalHooks } from '@/mixins/externalHooks'; import { getMappedResult } from '../utils/mappingUtils'; +type ParamterInputWrapperRef = InstanceType; + const DISPLAY_MODES_WITH_DATA_MAPPING = ['table', 'json', 'schema']; export default mixins(showMessage, externalHooks).extend({ @@ -219,18 +221,17 @@ export default mixins(showMessage, externalHooks).extend({ this.menuExpanded = expanded; }, optionSelected(command: string) { - if (this.$refs.param) { - (this.$refs.param as Vue).$emit('optionSelected', command); - } + const paramRef = this.$refs.param as ParamterInputWrapperRef | undefined; + paramRef?.$emit('optionSelected', command); }, valueChanged(parameterData: IUpdateInformation) { this.$emit('valueChanged', parameterData); }, onTextInput(parameterData: IUpdateInformation) { - const param = this.$refs.param as Vue | undefined; + const paramRef = this.$refs.param as ParamterInputWrapperRef | undefined; if (isValueExpression(this.parameter, parameterData.value)) { - param?.$emit('optionSelected', 'addExpression'); + paramRef?.$emit('optionSelected', 'addExpression'); } }, onDrop(newParamValue: string) { diff --git a/packages/editor-ui/src/components/ParameterInputWrapper.vue b/packages/editor-ui/src/components/ParameterInputWrapper.vue index 86e6ad9acba46..34bfb4d3ead4e 100644 --- a/packages/editor-ui/src/components/ParameterInputWrapper.vue +++ b/packages/editor-ui/src/components/ParameterInputWrapper.vue @@ -62,6 +62,8 @@ import { isValueExpression } from '@/utils'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; +type ParamRef = InstanceType; + export default mixins(showMessage, workflowHelpers).extend({ name: 'parameter-input-wrapper', components: { @@ -208,9 +210,9 @@ export default mixins(showMessage, workflowHelpers).extend({ this.$emit('drop', data); }, optionSelected(command: string) { - if (this.$refs.param) { - (this.$refs.param as Vue).$emit('optionSelected', command); - } + const paramRef = this.$refs.param as ParamRef | undefined; + + paramRef?.$emit('optionSelected', command); }, onValueChanged(parameterData: IUpdateInformation) { this.$emit('valueChanged', parameterData); diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue index 1b4f3f7c7635d..7ce47af422489 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocator.vue @@ -166,6 +166,8 @@ import { useRootStore } from '@/stores/n8nRootStore'; import { useNDVStore } from '@/stores/ndv'; import { useNodeTypesStore } from '@/stores/nodeTypes'; +type ResourceLocatorDropdownRef = InstanceType; + interface IResourceLocatorQuery { results: INodeListSearchItems[]; nextPageToken: unknown; @@ -407,9 +409,9 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ watch: { currentQueryError(curr: boolean, prev: boolean) { if (this.showResourceDropdown && curr && !prev) { - const input = this.$refs.input; - if (input) { - (input as HTMLElement).focus(); + const inputRef = this.$refs.input as HTMLInputElement | undefined; + if (inputRef) { + inputRef.focus(); } } }, @@ -445,7 +447,7 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ }, methods: { setWidth() { - const containerRef = this.$refs.container as HTMLElement; + const containerRef = this.$refs.container as HTMLElement | undefined; if (containerRef) { this.width = containerRef?.offsetWidth; } @@ -465,9 +467,9 @@ export default mixins(debounceHelper, workflowHelpers, nodeHelpers).extend({ this.trackEvent('User refreshed resource locator list'); }, onKeyDown(e: MouseEvent) { - const dropdown = this.$refs.dropdown; - if (dropdown && this.showResourceDropdown && !this.isSearchable) { - (dropdown as Vue).$emit('keyDown', e); + const dropdownRef = this.$refs.dropdown as ResourceLocatorDropdownRef | undefined; + if (dropdownRef && this.showResourceDropdown && !this.isSearchable) { + dropdownRef.$emit('keyDown', e); } }, openResource(url: string) { diff --git a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue index c26e8cbda8596..0b8c362e3af36 100644 --- a/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue +++ b/packages/editor-ui/src/components/ResourceLocator/ResourceLocatorDropdown.vue @@ -167,18 +167,21 @@ export default Vue.extend({ window.open(url, '_blank'); }, onKeyDown(e: KeyboardEvent) { - const container = this.$refs.resultsContainer as HTMLElement; + const containerRef = this.$refs.resultsContainer as HTMLElement | undefined; if (e.key === 'ArrowDown') { if (this.hoverIndex < this.sortedResources.length - 1) { this.hoverIndex++; - const items = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[]; - if (container && Array.isArray(items) && items.length === 1) { - const item = items[0]; - if (item.offsetTop + item.clientHeight > container.scrollTop + container.offsetHeight) { - const top = item.offsetTop - container.offsetHeight + item.clientHeight; - container.scrollTo({ top }); + const itemRefs = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[] | undefined; + if (containerRef && Array.isArray(itemRefs) && itemRefs.length === 1) { + const item = itemRefs[0]; + if ( + item.offsetTop + item.clientHeight > + containerRef.scrollTop + containerRef.offsetHeight + ) { + const top = item.offsetTop - containerRef.offsetHeight + item.clientHeight; + containerRef.scrollTo({ top }); } } } @@ -187,11 +190,11 @@ export default Vue.extend({ this.hoverIndex--; const searchOffset = this.filterable ? SEARCH_BAR_HEIGHT_PX : 0; - const items = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[]; - if (container && Array.isArray(items) && items.length === 1) { - const item = items[0]; - if (item.offsetTop <= container.scrollTop + searchOffset) { - container.scrollTo({ top: item.offsetTop - searchOffset }); + const itemRefs = this.$refs[`item-${this.hoverIndex}`] as HTMLElement[] | undefined; + if (containerRef && Array.isArray(itemRefs) && itemRefs.length === 1) { + const item = itemRefs[0]; + if (item.offsetTop <= containerRef.scrollTop + searchOffset) { + containerRef.scrollTo({ top: item.offsetTop - searchOffset }); } } } @@ -225,9 +228,10 @@ export default Vue.extend({ return; } - const container = this.$refs.resultsContainer as HTMLElement; - if (container) { - const diff = container.offsetHeight - (container.scrollHeight - container.scrollTop); + const containerRef = this.$refs.resultsContainer as HTMLElement | undefined; + if (containerRef) { + const diff = + containerRef.offsetHeight - (containerRef.scrollHeight - containerRef.scrollTop); if (diff > -SCROLL_MARGIN_PX && diff < SCROLL_MARGIN_PX) { this.$emit('loadMore'); } diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 8a97746285bc9..b5fc193e8efb9 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -1146,9 +1146,9 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten const previous = this.displayMode; this.ndvStore.setPanelDisplayMode({ pane: this.paneType, mode: displayMode }); - const dataContainer = this.$refs.dataContainer; - if (dataContainer) { - const dataDisplay = (dataContainer as Element).children[0]; + const dataContainerRef = this.$refs.dataContainer as Element | undefined; + if (dataContainerRef) { + const dataDisplay = dataContainerRef.children[0]; if (dataDisplay) { dataDisplay.scrollTo(0, 0); diff --git a/packages/editor-ui/src/components/RunDataTable.vue b/packages/editor-ui/src/components/RunDataTable.vue index 453f71cf3dead..a295ec913dc6a 100644 --- a/packages/editor-ui/src/components/RunDataTable.vue +++ b/packages/editor-ui/src/components/RunDataTable.vue @@ -178,6 +178,8 @@ import { getMappedExpression } from '@/utils/mappingUtils'; const MAX_COLUMNS_LIMIT = 40; +type DraggableRef = InstanceType; + export default mixins(externalHooks).extend({ name: 'run-data-table', components: { Draggable, MappingPill }, @@ -225,7 +227,7 @@ export default mixins(externalHooks).extend({ }, mounted() { if (this.tableData && this.tableData.columns && this.$refs.draggable) { - const tbody = (this.$refs.draggable as Vue).$refs.wrapper as HTMLElement; + const tbody = (this.$refs.draggable as DraggableRef).$refs.wrapper; if (tbody) { this.$emit('mounted', { avgRowHeight: tbody.offsetHeight / this.tableData.data.length, diff --git a/packages/editor-ui/src/components/TagsDropdown.vue b/packages/editor-ui/src/components/TagsDropdown.vue index a8a9b06d6b3f8..48a8c001c2aec 100644 --- a/packages/editor-ui/src/components/TagsDropdown.vue +++ b/packages/editor-ui/src/components/TagsDropdown.vue @@ -66,6 +66,11 @@ import { useUIStore } from '@/stores/ui'; import { useTagsStore } from '@/stores/tags'; import { EventBus } from '@/event-bus'; import { PropType } from 'vue'; +import { N8nOption, N8nSelect } from 'n8n-design-system'; + +type SelectRef = InstanceType; +type TagRef = InstanceType; +type CreateRef = InstanceType; const MANAGE_KEY = '__manage'; const CREATE_KEY = '__create'; @@ -74,7 +79,10 @@ export default mixins(showMessage).extend({ name: 'TagsDropdown', props: { placeholder: {}, - currentTagIds: {}, + currentTagIds: { + type: Array as PropType, + default: () => [], + }, createEnabled: {}, eventBus: { type: Object as PropType, @@ -90,10 +98,8 @@ export default mixins(showMessage).extend({ }; }, mounted() { - // @ts-ignore - const select = (this.$refs.select && - this.$refs.select.$refs && - this.$refs.select.$refs.innerSelect) as Vue | undefined; + const selectRef = this.$refs.select as SelectRef | undefined; + const select = selectRef?.$refs?.innerSelect; if (select) { const input = select.$refs.input as Element | undefined; if (input) { @@ -107,10 +113,8 @@ export default mixins(showMessage).extend({ this.$data.preventUpdate = true; this.$emit('blur'); - // @ts-ignore - if (this.$refs.select && typeof this.$refs.select.blur === 'function') { - // @ts-ignore - this.$refs.select.blur(); + if (typeof selectRef?.blur === 'function') { + selectRef.blur(); } } }); @@ -183,31 +187,27 @@ export default mixins(showMessage).extend({ } }, focusOnTopOption() { - const tags = this.$refs.tag as Vue[] | undefined; - const create = this.$refs.create as Vue | undefined; - //@ts-ignore // focus on create option - if (create && create.hoverItem) { - // @ts-ignore - create.hoverItem(); + const tagRefs = this.$refs.tag as TagRef[] | undefined; + const createRef = this.$refs.create as CreateRef | undefined; + // focus on create option + if (createRef && createRef.hoverItem) { + createRef.hoverItem(); } - //@ts-ignore // focus on top option after filter - else if (tags && tags[0] && tags[0].hoverItem) { - // @ts-ignore - tags[0].hoverItem(); + // focus on top option after filter + else if (tagRefs && tagRefs[0] && tagRefs[0].hoverItem) { + tagRefs[0].hoverItem(); } }, focusOnTag(tagId: string) { - const tagOptions = (this.$refs.tag as Vue[]) || []; + const tagOptions = (this.$refs.tag as TagRef[]) || []; if (tagOptions && tagOptions.length) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const added = tagOptions.find((ref: any) => ref.value === tagId); + const added = tagOptions.find((ref) => ref.value === tagId); } }, focusOnInput() { - const select = this.$refs.select as Vue | undefined; - if (select) { - // @ts-ignore - select.focusOnInput(); + const selectRef = this.$refs.select as SelectRef | undefined; + if (selectRef) { + selectRef.focusOnInput(); this.focused = true; } }, diff --git a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue index 76af2dd73955f..c228cc436a4fd 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue @@ -107,9 +107,14 @@ diff --git a/packages/editor-ui/src/components/PanelDragButton.vue b/packages/editor-ui/src/components/PanelDragButton.vue index 6413ff061e9e1..58acc80d9f09f 100644 --- a/packages/editor-ui/src/components/PanelDragButton.vue +++ b/packages/editor-ui/src/components/PanelDragButton.vue @@ -42,10 +42,10 @@ diff --git a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue index c228cc436a4fd..a93e25d9859d2 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTable.vue @@ -110,7 +110,7 @@ import { Table as ElTable } from 'element-ui'; import { MAX_TAG_NAME_LENGTH } from '@/constants'; import { ITagRow } from '@/Interface'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { N8nInput } from 'n8n-design-system'; type TableRef = InstanceType; @@ -119,7 +119,7 @@ type N8nInputRef = InstanceType; const INPUT_TRANSITION_TIMEOUT = 350; const DELETE_TRANSITION_TIMEOUT = 100; -export default Vue.extend({ +export default defineComponent({ name: 'TagsTable', props: ['rows', 'isLoading', 'newName', 'isSaving'], data() { diff --git a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue index a7079d0232e5e..543fd1b213d3e 100644 --- a/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue +++ b/packages/editor-ui/src/components/TagsManager/TagsView/TagsTableHeader.vue @@ -29,9 +29,9 @@ diff --git a/packages/editor-ui/src/composables/useExternalHooks.ts b/packages/editor-ui/src/composables/useExternalHooks.ts index ff3e3ab48123c..cd9b1049b5060 100644 --- a/packages/editor-ui/src/composables/useExternalHooks.ts +++ b/packages/editor-ui/src/composables/useExternalHooks.ts @@ -1,12 +1,12 @@ -import { IExternalHooks } from '@/Interface'; -import { IDataObject } from 'n8n-workflow'; +import type { IExternalHooks } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; import { useWebhooksStore } from '@/stores'; -import { runExternalHook } from '@/mixins/externalHooks'; +import { runExternalHook } from '@/utils'; export function useExternalHooks(): IExternalHooks { return { async run(eventName: string, metadata?: IDataObject): Promise { - return await runExternalHook.call(this, eventName, useWebhooksStore(), metadata); + return await runExternalHook(eventName, useWebhooksStore(), metadata); }, }; } diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index b95beda7f2876..c25f6cc79d1f6 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -19,16 +19,14 @@ import '@fontsource/open-sans/latin-700.css'; import App from '@/App.vue'; import router from './router'; -import { runExternalHook } from '@/mixins/externalHooks'; +import { runExternalHook } from '@/utils'; import { TelemetryPlugin } from './plugins/telemetry'; import { I18nPlugin, i18nInstance } from './plugins/i18n'; import { createPinia, PiniaVuePlugin } from 'pinia'; -import { useWebhooksStore } from './stores/webhooks'; -import { useUsersStore } from './stores/users'; +import { useWebhooksStore, useUsersStore } from '@/stores'; import { VIEWS } from '@/constants'; -import { useUIStore } from './stores/ui'; Vue.config.productionTip = false; diff --git a/packages/editor-ui/src/mixins/copyPaste.ts b/packages/editor-ui/src/mixins/copyPaste.ts index eee52394d99ac..49c4ae830d233 100644 --- a/packages/editor-ui/src/mixins/copyPaste.ts +++ b/packages/editor-ui/src/mixins/copyPaste.ts @@ -2,10 +2,10 @@ * Captures any pasted data and sends it to method "receivedCopyPasteData" which has to be * defined on the component which uses this mixin */ -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { debounce } from 'lodash-es'; -export const copyPaste = Vue.extend({ +export const copyPaste = defineComponent({ data() { return { copyPasteElementsGotCreated: false, diff --git a/packages/editor-ui/src/mixins/debounce.ts b/packages/editor-ui/src/mixins/debounce.ts index 7c491780aaf3d..217db45d7a66e 100644 --- a/packages/editor-ui/src/mixins/debounce.ts +++ b/packages/editor-ui/src/mixins/debounce.ts @@ -1,7 +1,7 @@ import { debounce } from 'lodash-es'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export const debounceHelper = Vue.extend({ +export const debounceHelper = defineComponent({ data() { return { debouncedFunctions: [] as any[], diff --git a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts b/packages/editor-ui/src/mixins/deviceSupportHelpers.ts index 6b316bf87d8f1..a8f87f8820aee 100644 --- a/packages/editor-ui/src/mixins/deviceSupportHelpers.ts +++ b/packages/editor-ui/src/mixins/deviceSupportHelpers.ts @@ -1,6 +1,6 @@ -import Vue from 'vue'; +import { defineComponent } from 'vue'; -export const deviceSupportHelpers = Vue.extend({ +export const deviceSupportHelpers = defineComponent({ data() { return { // @ts-ignore msMaxTouchPoints is deprecated but must fix tablet bugs before fixing this.. otherwise breaks touchscreen computers diff --git a/packages/editor-ui/src/mixins/externalHooks.ts b/packages/editor-ui/src/mixins/externalHooks.ts index 5a336ae125ac8..09b180b039641 100644 --- a/packages/editor-ui/src/mixins/externalHooks.ts +++ b/packages/editor-ui/src/mixins/externalHooks.ts @@ -1,35 +1,10 @@ -import { IExternalHooks } from '@/Interface'; +import type { IExternalHooks } from '@/Interface'; +import type { IDataObject } from 'n8n-workflow'; import { useWebhooksStore } from '@/stores/webhooks'; -import { IDataObject } from 'n8n-workflow'; -import { Store } from 'pinia'; -import Vue from 'vue'; +import { defineComponent } from 'vue'; +import { runExternalHook } from '@/utils'; -declare global { - interface Window { - n8nExternalHooks?: Record< - string, - Record Promise>> - >; - } -} - -export async function runExternalHook(eventName: string, store: Store, metadata?: IDataObject) { - if (!window.n8nExternalHooks) { - return; - } - - const [resource, operator] = eventName.split('.'); - - if (window.n8nExternalHooks[resource]?.[operator]) { - const hookMethods = window.n8nExternalHooks[resource][operator]; - - for (const hookMethod of hookMethods) { - await hookMethod(store, metadata); - } - } -} - -export const externalHooks = Vue.extend({ +export const externalHooks = defineComponent({ methods: { $externalHooks(): IExternalHooks { return { diff --git a/packages/editor-ui/src/mixins/restApi.ts b/packages/editor-ui/src/mixins/restApi.ts index f14d197815cc4..7663eb1a7b204 100644 --- a/packages/editor-ui/src/mixins/restApi.ts +++ b/packages/editor-ui/src/mixins/restApi.ts @@ -1,4 +1,4 @@ -import Vue from 'vue'; +import { defineComponent } from 'vue'; import { parse } from 'flatted'; import { Method } from 'axios'; @@ -55,7 +55,7 @@ function unflattenExecutionData(fullExecutionData: IExecutionFlattedResponse): I return returnData; } -export const restApi = Vue.extend({ +export const restApi = defineComponent({ computed: { ...mapStores(useRootStore), }, diff --git a/packages/editor-ui/src/mixins/userHelpers.ts b/packages/editor-ui/src/mixins/userHelpers.ts index 8ac4d45e0341f..ecf4bd6d9da8a 100644 --- a/packages/editor-ui/src/mixins/userHelpers.ts +++ b/packages/editor-ui/src/mixins/userHelpers.ts @@ -1,10 +1,10 @@ -import { IPermissions, IUser } from '@/Interface'; +import type { IPermissions } from '@/Interface'; import { isAuthorized } from '@/utils'; import { useUsersStore } from '@/stores/users'; -import Vue from 'vue'; -import { Route } from 'vue-router'; +import { defineComponent } from 'vue'; +import type { Route } from 'vue-router'; -export const userHelpers = Vue.extend({ +export const userHelpers = defineComponent({ methods: { canUserAccessRouteByName(name: string): boolean { const { route } = this.$router.resolve({ name }); diff --git a/packages/editor-ui/src/shims.d.ts b/packages/editor-ui/src/shims.d.ts index c231305c43c1c..fc95a6f17da9c 100644 --- a/packages/editor-ui/src/shims.d.ts +++ b/packages/editor-ui/src/shims.d.ts @@ -1,4 +1,6 @@ import Vue, { VNode } from 'vue'; +import type { Store } from 'pinia'; +import type { IDataObject } from 'n8n-workflow'; declare module 'markdown-it-link-attributes'; declare module 'markdown-it-emoji'; @@ -17,6 +19,10 @@ declare global { interface Window { BASE_PATH: string; REST_ENDPOINT: string; + n8nExternalHooks?: Record< + string, + Record Promise>> + >; } namespace JSX { diff --git a/packages/editor-ui/src/stores/nodeCreator.ts b/packages/editor-ui/src/stores/nodeCreator.ts index 49bf4e0a587dd..f3709cd5e1d35 100644 --- a/packages/editor-ui/src/stores/nodeCreator.ts +++ b/packages/editor-ui/src/stores/nodeCreator.ts @@ -20,11 +20,12 @@ import { } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes'; import { useWorkflowsStore } from './workflows'; -import { CUSTOM_API_CALL_KEY, ALL_NODE_FILTER } from '@/constants'; +import { CUSTOM_API_CALL_KEY } from '@/constants'; import { INodeCreatorState, INodeFilterType, IUpdateInformation } from '@/Interface'; -import { BaseTextKey, i18n } from '@/plugins/i18n'; -import { externalHooks } from '@/mixins/externalHooks'; +import { i18n } from '@/plugins/i18n'; import { Telemetry } from '@/plugins/telemetry'; +import { runExternalHook } from '@/utils'; +import { useWebhooksStore } from '@/stores/webhooks'; const PLACEHOLDER_RECOMMENDED_ACTION_KEY = 'placeholder_recommended'; @@ -286,14 +287,12 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, { return storeWatcher; }, trackActionSelected(action: IUpdateInformation, telemetry?: Telemetry) { - const { $externalHooks } = new externalHooks(); - const payload = { node_type: action.key, action: action.name, resource: (action.value as INodeParameters).resource || '', }; - $externalHooks().run('nodeCreateList.addAction', payload); + runExternalHook('nodeCreateList.addAction', useWebhooksStore(), payload); telemetry?.trackNodesPanel('nodeCreateList.addAction', payload); }, }, diff --git a/packages/editor-ui/src/utils/externalHooks.ts b/packages/editor-ui/src/utils/externalHooks.ts new file mode 100644 index 0000000000000..2cb48bc76988d --- /dev/null +++ b/packages/editor-ui/src/utils/externalHooks.ts @@ -0,0 +1,18 @@ +import type { IDataObject } from 'n8n-workflow'; +import type { Store } from 'pinia'; + +export async function runExternalHook(eventName: string, store: Store, metadata?: IDataObject) { + if (!window.n8nExternalHooks) { + return; + } + + const [resource, operator] = eventName.split('.'); + + if (window.n8nExternalHooks[resource]?.[operator]) { + const hookMethods = window.n8nExternalHooks[resource][operator]; + + for (const hookMethod of hookMethods) { + await hookMethod(store, metadata); + } + } +} diff --git a/packages/editor-ui/src/utils/index.ts b/packages/editor-ui/src/utils/index.ts index 8cb8b0281681a..f5b1bdb5c1ad6 100644 --- a/packages/editor-ui/src/utils/index.ts +++ b/packages/editor-ui/src/utils/index.ts @@ -1,5 +1,6 @@ export * from './apiUtils'; export * from './canvasUtils'; +export * from './externalHooks'; export * from './htmlUtils'; export * from './nodeTypesUtils'; export * from './sortUtils'; diff --git a/packages/editor-ui/src/views/AuthView.vue b/packages/editor-ui/src/views/AuthView.vue index 611090cbd4cba..7a3fa27ac7f7d 100644 --- a/packages/editor-ui/src/views/AuthView.vue +++ b/packages/editor-ui/src/views/AuthView.vue @@ -22,12 +22,12 @@ diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index 8899ab74f6c7b..c775580dddebc 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -1,11 +1,11 @@ + + diff --git a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts index a11ff18670bcd..6bc61ee8ac119 100644 --- a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts +++ b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts @@ -73,6 +73,10 @@ export class CrateDb implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts index a806e5afacc33..0e78130098a31 100644 --- a/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Google/BigQuery/v2/actions/database/executeQuery.operation.ts @@ -14,6 +14,9 @@ const properties: INodeProperties[] = [ displayName: 'SQL Query', name: 'sqlQuery', type: 'string', + typeOptions: { + editor: 'sqlEditor', + }, displayOptions: { hide: { '/options.useLegacySql': [true], @@ -28,6 +31,9 @@ const properties: INodeProperties[] = [ displayName: 'SQL Query', name: 'sqlQuery', type: 'string', + typeOptions: { + editor: 'sqlEditor', + }, displayOptions: { show: { '/options.useLegacySql': [true], diff --git a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts index 8c7c00dee2787..14a39fdddb455 100644 --- a/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts +++ b/packages/nodes-base/nodes/Microsoft/Sql/MicrosoftSql.node.ts @@ -90,6 +90,10 @@ export class MicrosoftSql implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'mssql', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts b/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts index 21759b3e45872..f54807ef412c9 100644 --- a/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts +++ b/packages/nodes-base/nodes/MySql/v1/MySqlV1.node.ts @@ -72,6 +72,10 @@ const versionDescription: INodeTypeDescription = { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'mysql', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts index bf247596d30a8..853be02ac3822 100644 --- a/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/MySql/v2/actions/database/executeQuery.operation.ts @@ -21,7 +21,8 @@ const properties: INodeProperties[] = [ description: "The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.", typeOptions: { - rows: 3, + editor: 'sqlEditor', + sqlDialect: 'mysql', }, hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks', }, diff --git a/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts b/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts index ac98d867336e5..b9190a59451cb 100644 --- a/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts +++ b/packages/nodes-base/nodes/Postgres/v1/PostgresV1.node.ts @@ -71,6 +71,10 @@ const versionDescription: INodeTypeDescription = { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts index 41aedcf156251..1d59228f0b98c 100644 --- a/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts +++ b/packages/nodes-base/nodes/Postgres/v2/actions/database/executeQuery.operation.ts @@ -21,7 +21,8 @@ const properties: INodeProperties[] = [ description: "The SQL query to execute. You can use n8n expressions and $1, $2, $3, etc to refer to the 'Query Parameters' set in options below.", typeOptions: { - rows: 3, + editor: 'sqlEditor', + sqlDialect: 'postgres', }, hint: 'Prefer using query parameters over n8n expressions to avoid SQL injection attacks', }, diff --git a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts index 5987d4007eda1..90af31d753afa 100644 --- a/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts +++ b/packages/nodes-base/nodes/QuestDb/QuestDb.node.ts @@ -60,6 +60,10 @@ export class QuestDb implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts index 8264cdce7a83c..32ea9e477ff65 100644 --- a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts +++ b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts @@ -65,6 +65,9 @@ export class Snowflake implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts index 5a43f7c500f53..1d0c2c21ac650 100644 --- a/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts +++ b/packages/nodes-base/nodes/TimescaleDb/TimescaleDb.node.ts @@ -65,6 +65,10 @@ export class TimescaleDb implements INodeType { displayName: 'Query', name: 'query', type: 'string', + typeOptions: { + editor: 'sqlEditor', + sqlDialect: 'postgres', + }, displayOptions: { show: { operation: ['executeQuery'], diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 96898dec2df13..9cdfd57e8995c 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1019,8 +1019,9 @@ export type NodePropertyTypes = export type CodeAutocompleteTypes = 'function' | 'functionItem'; -export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'json'; +export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'sqlEditor' | 'json'; export type CodeNodeEditorLanguage = 'javaScript' | 'json'; //| 'python' | 'sql'; +export type SQLDialect = 'mssql' | 'mysql' | 'postgres'; export interface ILoadOptions { routing?: { @@ -1035,6 +1036,7 @@ export interface INodePropertyTypeOptions { codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string editor?: EditorType; // Supported by: string editorLanguage?: CodeNodeEditorLanguage; // Supported by: string in combination with editor: codeNodeEditor + sqlDialect?: SQLDialect; // Supported by: sqlEditor loadOptionsDependsOn?: string[]; // Supported by: options loadOptionsMethod?: string; // Supported by: options loadOptions?: ILoadOptions; // Supported by: options diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c3c5ded5d4d82..77412c03700e9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -844,6 +844,9 @@ importers: '@codemirror/lang-json': specifier: ^6.0.1 version: 6.0.1 + '@codemirror/lang-sql': + specifier: ^6.4.1 + version: 6.4.1(@codemirror/view@6.5.1)(@lezer/common@1.0.1) '@codemirror/language': specifier: ^6.2.1 version: 6.2.1 @@ -3367,6 +3370,19 @@ packages: '@lezer/json': 1.0.0 dev: false + /@codemirror/lang-sql@6.4.1(@codemirror/view@6.5.1)(@lezer/common@1.0.1): + resolution: {integrity: sha512-PFB56L+A0WGY35uRya+Trt5g19V9k2V9X3c55xoFW4RgiATr/yLqWsbbnEsdxuMn5tLpuikp7Kmj9smRsqBXAg==} + dependencies: + '@codemirror/autocomplete': 6.4.0(@codemirror/language@6.2.1)(@codemirror/state@6.1.4)(@codemirror/view@6.5.1)(@lezer/common@1.0.1) + '@codemirror/language': 6.2.1 + '@codemirror/state': 6.1.4 + '@lezer/highlight': 1.1.1 + '@lezer/lr': 1.2.3 + transitivePeerDependencies: + - '@codemirror/view' + - '@lezer/common' + dev: false + /@codemirror/language@6.2.1: resolution: {integrity: sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==} dependencies: From 6335e0938d469ec2c891e0da4ea2b9918d132bee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 25 Apr 2023 16:18:46 +0000 Subject: [PATCH 036/110] fix(editor): Make the frontend work again when `NODE_FUNCTION_ALLOW_EXTERNAL` is set (no-changelog) (#6058) --- packages/cli/src/config/index.ts | 1 + packages/editor-ui/src/stores/settings.ts | 7 ++----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index 4b87524611196..6f20ba3fb9e0f 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -16,6 +16,7 @@ if (inE2ETests) { N8N_PUBLIC_API_DISABLED: 'true', EXTERNAL_FRONTEND_HOOKS_URLS: '', N8N_PERSONALIZATION_ENABLED: 'false', + NODE_FUNCTION_ALLOW_EXTERNAL: 'node-fetch', }; } else if (inTest) { process.env.N8N_PUBLIC_API_DISABLED = 'true'; diff --git a/packages/editor-ui/src/stores/settings.ts b/packages/editor-ui/src/stores/settings.ts index 12e7edcc901a2..65fb38b137a1f 100644 --- a/packages/editor-ui/src/stores/settings.ts +++ b/packages/editor-ui/src/stores/settings.ts @@ -235,11 +235,8 @@ export const useSettingsStore = defineStore(STORES.SETTINGS, { setPromptsData(promptsData: IN8nPrompts): void { Vue.set(this, 'promptsData', promptsData); }, - setAllowedModules(allowedModules: { builtIn?: string; external?: string }): void { - this.settings.allowedModules = { - ...(allowedModules.builtIn && { builtIn: allowedModules.builtIn.split(',') }), - ...(allowedModules.external && { external: allowedModules.external.split(',') }), - }; + setAllowedModules(allowedModules: { builtIn?: string[]; external?: string[] }): void { + this.settings.allowedModules = allowedModules; }, async fetchPromptsData(): Promise { if (!this.isTelemetryEnabled) { From 390841bbf0fdd4d536101593711a6658ea2784e4 Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Wed, 26 Apr 2023 09:18:10 +0200 Subject: [PATCH 037/110] feat(editor): Enhance Node Creator actions view (#5954) * WIP * WIP * Extract actions into composable * WIP: Preserve categories when searching * WIP * WIP: Tweak styles * WIP: Refactor node creator * WIP: Finish Node Creator node view/subcategories refactor * WIP: Finished actions refactor * Cleanup & Lintfix * WIP: Improve memory managment * Fix interactions * WIP * WIP: Keyboard navigation * Improve keyboard navigation and memory managment * Finished view refactor * FIx custom api calls and activation callouts * Fix actions tracking and cleanup * Product review fixes * Telemetry fixes * Fix node creator e2es * Set action name font size and actionsEmpty font weight * Fix failing credentials spec * Make sure to select first action item when switching from nodes panel to actions panel * Add actions panel e2e tests * Cleanup * Fix actions generation and cleanup * Add correct Learn More link and adjust displaying of trigger icon * Change trigger icon condition to use nodeType group * Cleanup nodeTypesUtils and snapshots and lintfixes * Lint fixes * Refine logic to show trigger icon in node creator * Add unit tests & clean up * Add `003_auto_insert_action` experiment, hide empty sections for opposite root view * Lintfix * Do not show empty category tooltips and only show activation callout in triger root view * Fix no-results node creator view * Spacings tweaks and root rendering logic adjustment * Add unit tests * Lint and e2e fixes * Revert CLI changes, fix unit tests * Remove useless comments * Sync master, replace $externalHooks mixin * Lint fix * Focus first action when panel slides in, not category * Address PR comments * Lint fix * Remove `setAddedNodeActionParameters` optional track param * Further simplify setAddedNodeActionParameters * Fix pnpn lock file * Fix types imports * Fix 13-pinning spec --- cypress/e2e/11-inline-expression-editor.cy.ts | 2 +- cypress/e2e/13-pinning.cy.ts | 4 +- cypress/e2e/4-node-creator.cy.ts | 109 ++- cypress/e2e/9-expression-editor-modal.cy.ts | 2 +- cypress/pages/features/node-creator.ts | 6 +- cypress/pages/workflow.ts | 3 +- .../src/components/N8nCallout/Callout.vue | 24 +- .../__snapshots__/Callout.spec.ts.snap | 14 +- .../N8nNodeCreatorNode/NodeCreatorNode.vue | 11 +- .../src/components/N8nNodeIcon/NodeIcon.vue | 7 +- packages/editor-ui/package.json | 3 +- packages/editor-ui/src/Interface.ts | 93 ++- .../Node/NodeCreator/CategorizedItems.vue | 690 ------------------ .../Node/NodeCreator/CategoryItem.vue | 41 -- .../Node/NodeCreator/ItemIterator.vue | 210 ------ .../{ => ItemTypes}/ActionItem.vue | 70 +- .../NodeCreator/ItemTypes/CategoryItem.vue | 81 ++ .../Node/NodeCreator/ItemTypes/LabelItem.vue | 31 + .../NodeCreator/{ => ItemTypes}/NodeItem.vue | 100 +-- .../{ => ItemTypes}/SubcategoryItem.vue | 11 +- .../NodeCreator/{ => ItemTypes}/ViewItem.vue | 22 +- .../components/Node/NodeCreator/MainPanel.vue | 405 ---------- .../Node/NodeCreator/Modes/ActionsMode.vue | 366 ++++++++++ .../Node/NodeCreator/Modes/NodesMode.vue | 213 ++++++ .../Node/NodeCreator/NodeCreator.vue | 78 +- .../NodeCreator/{ => Panel}/NoResults.vue | 25 +- .../NodeCreator/{ => Panel}/NoResultsIcon.vue | 0 .../Node/NodeCreator/Panel/NodesListPanel.vue | 258 +++++++ .../NodeCreator/{ => Panel}/SearchBar.vue | 9 +- .../Renderers/CategorizedItemsRenderer.vue | 153 ++++ .../NodeCreator/Renderers/ItemsRenderer.vue | 215 ++++++ .../__tests__/CategoryItem.test.ts | 31 + .../__tests__/ItemsRenderer.test.ts | 82 +++ .../__tests__/NodesListPanel.test.ts | 286 ++++++++ .../__tests__/useKeyboardNavigation.test.ts | 90 +++ .../Node/NodeCreator/__tests__/utils.ts | 126 ++++ .../NodeCreator/composables/useActions.ts | 218 ++++++ .../composables/useActionsGeneration.ts | 279 +++++++ .../composables/useKeyboardNavigation.ts | 156 ++++ .../NodeCreator/composables/useViewStacks.ts | 188 +++++ .../Node/NodeCreator/useMainPanelView.ts | 198 ----- .../src/components/Node/NodeCreator/utils.ts | 73 ++ .../components/Node/NodeCreator/viewsData.ts | 168 +++++ .../transitions/SlideTransition.vue | 2 +- packages/editor-ui/src/constants.ts | 35 +- packages/editor-ui/src/main.ts | 1 + .../src/plugins/i18n/locales/en.json | 34 +- .../editor-ui/src/plugins/telemetry/index.ts | 9 +- packages/editor-ui/src/stores/nodeCreator.ts | 427 +---------- packages/editor-ui/src/stores/nodeTypes.ts | 22 +- .../editor-ui/src/utils/nodeTypesUtils.ts | 193 ----- packages/editor-ui/src/views/NodeView.vue | 12 +- packages/workflow/src/Interfaces.ts | 7 - pnpm-lock.yaml | 44 +- 54 files changed, 3488 insertions(+), 2449 deletions(-) delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/CategorizedItems.vue delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/CategoryItem.vue delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemIterator.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/ActionItem.vue (73%) create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/CategoryItem.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/ItemTypes/LabelItem.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/NodeItem.vue (66%) rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/SubcategoryItem.vue (77%) rename packages/editor-ui/src/components/Node/NodeCreator/{ => ItemTypes}/ViewItem.vue (52%) delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/MainPanel.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Modes/ActionsMode.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Modes/NodesMode.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => Panel}/NoResults.vue (66%) rename packages/editor-ui/src/components/Node/NodeCreator/{ => Panel}/NoResultsIcon.vue (100%) create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Panel/NodesListPanel.vue rename packages/editor-ui/src/components/Node/NodeCreator/{ => Panel}/SearchBar.vue (91%) create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Renderers/CategorizedItemsRenderer.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/Renderers/ItemsRenderer.vue create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/CategoryItem.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/ItemsRenderer.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/NodesListPanel.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/useKeyboardNavigation.test.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/__tests__/utils.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useActions.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useActionsGeneration.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useKeyboardNavigation.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/composables/useViewStacks.ts delete mode 100644 packages/editor-ui/src/components/Node/NodeCreator/useMainPanelView.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/utils.ts create mode 100644 packages/editor-ui/src/components/Node/NodeCreator/viewsData.ts diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index de5594a4f429a..88fea311d92cb 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -69,6 +69,6 @@ describe('Inline expression editor', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); - WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^getAll$/); + WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^get$/); }); }); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index 6f1c328fd9c6b..c278231017f9d 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -1,6 +1,6 @@ import { HTTP_REQUEST_NODE_NAME, - MANUAL_TRIGGER_NODE_DISPLAY_NAME, + MANUAL_TRIGGER_NODE_NAME, PIPEDRIVE_NODE_NAME, SET_NODE_NAME, } from '../constants'; @@ -75,7 +75,7 @@ describe('Data pinning', () => { }); it('Should be able to reference paired items in a node located before pinned data', () => { - workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_DISPLAY_NAME); + workflowPage.actions.addInitialNodeToCanvas(MANUAL_TRIGGER_NODE_NAME); workflowPage.actions.addNodeToCanvas(HTTP_REQUEST_NODE_NAME, true, true); ndv.actions.setPinnedData([{ http: 123 }]); ndv.actions.close(); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index 0057afd93c839..0ad0306cb6b93 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -58,8 +58,8 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.getCreatorItem('On app event').click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image'); - nodeCreatorFeature.getters.getCreatorItem('Results in other categories (1)').should('exist'); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.getCategoryItem('Results in other categories').should('exist'); + nodeCreatorFeature.getters.creatorItem().should('have.length', 1); nodeCreatorFeature.getters.getCreatorItem('Edit Image').should('exist'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('edit image123123'); nodeCreatorFeature.getters.creatorItem().should('have.length', 0); @@ -101,7 +101,7 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.activeSubcategory().should('have.text', 'FTP'); nodeCreatorFeature.getters.searchBar().find('input').clear().type('file'); // Navigate to rename action which should be the 4th item - nodeCreatorFeature.getters.searchBar().find('input').type('{downarrow} {downarrow} {downarrow} {rightarrow}'); + nodeCreatorFeature.getters.searchBar().find('input').type('{uparrow}{uparrow}{rightarrow}'); NDVModal.getters.parameterInput('operation').should('contain.text', 'Rename'); }) @@ -127,9 +127,107 @@ describe('Node Creator', () => { }) nodeCreatorFeature.getters.searchBar().find('input').clear().type(doubleActionNode); nodeCreatorFeature.getters.getCreatorItem(doubleActionNode).click(); - nodeCreatorFeature.getters.creatorItem().should('have.length', 2); + nodeCreatorFeature.getters.creatorItem().should('have.length', 4); }) + it('should have "Actions" section collapsed when opening actions view from Trigger root view', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('ActiveCampaign'); + nodeCreatorFeature.getters.getCreatorItem('ActiveCampaign').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').should('exist'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').should('exist'); + + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed', 'true'); + nodeCreatorFeature.getters.getCategoryItem('Actions').click() + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); + }); + + it('should have "Triggers" section collapsed when opening actions view from Regular root view', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.getCreatorItem('Manually').click(); + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); + nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('not.have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Actions').click() + nodeCreatorFeature.getters.getCategoryItem('Actions').parent().should('have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('have.attr', 'data-category-collapsed'); + nodeCreatorFeature.getters.getCategoryItem('Triggers').click() + nodeCreatorFeature.getters.getCategoryItem('Triggers').parent().should('not.have.attr', 'data-category-collapsed'); + }); + + it('should show callout and two suggested nodes if node has no trigger actions', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible'); + }); + + it('should show intro callout if user has not made a production execution', () => { + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-activation-callout').should('be.visible'); + nodeCreatorFeature.getters.activeSubcategory().find('button').click(); + nodeCreatorFeature.getters.searchBar().find('input').clear() + + nodeCreatorFeature.getters.getCreatorItem('On a schedule').click(); + + // Setup 1s interval execution + cy.getByTestId('parameter-input-field').click(); + cy.getByTestId('parameter-input-field') + .find('.el-select-dropdown') + .find('.option-headline') + .contains('Seconds') + .click(); + cy.getByTestId('parameter-input-secondsInterval').clear().type('1'); + + NDVModal.actions.close(); + + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + nodeCreatorFeature.getters.getCreatorItem('Get All People').click(); + NDVModal.actions.close(); + + WorkflowPage.actions.saveWorkflowOnButtonClick(); + WorkflowPage.actions.activateWorkflow(); + WorkflowPage.getters.activatorSwitch().should('have.class', 'is-checked'); + + // Wait for schedule 1s execution to mark user as having made a production execution + cy.wait(1500); + cy.reload() + + // Action callout should not be visible after user has made a production execution + nodeCreatorFeature.actions.openNodeCreator(); + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + cy.getByTestId('actions-panel-activation-callout').should('not.exist'); + }); + + it('should show Trigger and Actions sections during search', () => { + nodeCreatorFeature.actions.openNodeCreator(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Customer Datastore (n8n training)'); + nodeCreatorFeature.getters.getCreatorItem('Customer Datastore (n8n training)').click(); + + nodeCreatorFeature.getters.searchBar().find('input').clear().type('Non existent action name'); + + nodeCreatorFeature.getters.getCategoryItem('Triggers').should('be.visible'); + nodeCreatorFeature.getters.getCategoryItem('Actions').should('be.visible'); + cy.getByTestId('actions-panel-no-triggers-callout').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Schedule').should('be.visible'); + nodeCreatorFeature.getters.getCreatorItem('On a Webhook call').should('be.visible'); + }); + describe('should correctly append manual trigger for regular actions', () => { // For these sources, manual node should be added const sourcesWithAppend = [ @@ -152,6 +250,7 @@ describe('Node Creator', () => { source.handler() nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.getters.canvasNodes().should('have.length', 2); @@ -162,12 +261,14 @@ describe('Node Creator', () => { nodeCreatorFeature.getters.canvasAddButton().click(); nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.actions.deleteNode('When clicking "Execute Workflow"') WorkflowPage.getters.canvasNodePlusEndpointByName('n8n').click() nodeCreatorFeature.getters.searchBar().find('input').clear().type('n8n'); nodeCreatorFeature.getters.getCreatorItem('n8n').click(); + nodeCreatorFeature.getters.getCategoryItem('Actions').click(); nodeCreatorFeature.getters.getCreatorItem('Create a credential').click(); NDVModal.actions.close(); WorkflowPage.getters.canvasNodes().should('have.length', 2); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index 957c0505f5505..dd4e01128b134 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -60,6 +60,6 @@ describe('Expression editor modal', () => { it('should resolve $parameter[]', () => { WorkflowPage.getters.expressionModalInput().clear(); WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]'); - WorkflowPage.getters.expressionModalOutput().contains(/^getAll$/); + WorkflowPage.getters.expressionModalOutput().contains(/^get$/); }); }); diff --git a/cypress/pages/features/node-creator.ts b/cypress/pages/features/node-creator.ts index 8ebe6db702d6e..6686de25ff1fe 100644 --- a/cypress/pages/features/node-creator.ts +++ b/cypress/pages/features/node-creator.ts @@ -7,6 +7,7 @@ export class NodeCreator extends BasePage { plusButton: () => cy.getByTestId('node-creator-plus-button'), canvasAddButton: () => cy.getByTestId('canvas-add-button'), searchBar: () => cy.getByTestId('search-bar'), + getCategoryItem: (label: string) => cy.get(`[data-keyboard-nav-id="${label}"]`), getCreatorItem: (label: string) => this.getters.creatorItem().contains(label).parents('[data-test-id="item-iterator-item"]'), getNthCreatorItem: (n: number) => this.getters.creatorItem().eq(n), @@ -15,10 +16,11 @@ export class NodeCreator extends BasePage { selectedTab: () => this.getters.nodeCreatorTabs().find('.is-active'), categorizedItems: () => cy.getByTestId('categorized-items'), creatorItem: () => cy.getByTestId('item-iterator-item'), + categoryItem: () => cy.getByTestId('node-creator-category-item'), communityNodeTooltip: () => cy.getByTestId('node-item-community-tooltip'), - noResults: () => cy.getByTestId('categorized-no-results'), + noResults: () => cy.getByTestId('node-creator-no-results'), nodeItemName: () => cy.getByTestId('node-creator-item-name'), - activeSubcategory: () => cy.getByTestId('categorized-items-subcategory'), + activeSubcategory: () => cy.getByTestId('nodes-list-header'), expandedCategories: () => this.getters.creatorItem().find('>div').filter('.active').invoke('text'), }; diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index d42b2850c4e93..8b6d195105248 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -140,7 +140,8 @@ export class WorkflowPage extends BasePage { if(action) { cy.contains(action).click() } else { - cy.getByTestId('item-iterator-item').eq(1).click() + // Select the first action + cy.get('[data-keyboard-nav-type="action"]').eq(0).click() } } }) diff --git a/packages/design-system/src/components/N8nCallout/Callout.vue b/packages/design-system/src/components/N8nCallout/Callout.vue index 97c15296e0558..604cec6a25a42 100644 --- a/packages/design-system/src/components/N8nCallout/Callout.vue +++ b/packages/design-system/src/components/N8nCallout/Callout.vue @@ -1,7 +1,7 @@