gallyga commited on
Commit
aec3094
·
1 Parent(s): ccbea5f

Add n8n Chinese version

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .bundlemonrc.json +30 -0
  2. .cursorindexingignore +3 -0
  3. .devcontainer/Dockerfile +7 -0
  4. .devcontainer/devcontainer.json +19 -0
  5. .devcontainer/docker-compose.yml +24 -0
  6. .dockerignore +20 -0
  7. .editorconfig +20 -0
  8. .env.enterprise-mock +45 -0
  9. .env.example +91 -0
  10. .git-blame-ignore-revs +18 -0
  11. .gitignore +29 -0
  12. .npmignore +28 -0
  13. .npmrc +14 -0
  14. .prettierignore +23 -0
  15. .prettierrc.js +51 -0
  16. .vscode/extensions.json +14 -0
  17. .vscode/settings.default.json +37 -0
  18. CHANGELOG.md +0 -0
  19. CODE_OF_CONDUCT.md +76 -0
  20. CONTRIBUTING.md +315 -0
  21. CONTRIBUTOR_LICENSE_AGREEMENT.md +5 -0
  22. DEPLOYMENT-SUMMARY.md +158 -0
  23. ENTERPRISE-MOCK-README.md +202 -0
  24. LICENSE.md +88 -0
  25. LICENSE_EE.md +27 -0
  26. README-HUGGINGFACE.md +160 -0
  27. SECURITY.md +4 -0
  28. assets/n8n-logo.png +3 -0
  29. assets/n8n-screenshot-readme.png +3 -0
  30. assets/n8n-screenshot.png +3 -0
  31. biome.jsonc +53 -0
  32. codecov.yml +67 -0
  33. correct_compare.js +110 -0
  34. cypress/.eslintrc.js +40 -0
  35. cypress/.gitignore +3 -0
  36. cypress/README.md +32 -0
  37. cypress/augmentation.d.ts +4 -0
  38. cypress/biome.jsonc +7 -0
  39. cypress/composables/becomeTemplateCreatorCta.ts +18 -0
  40. cypress/composables/create.ts +19 -0
  41. cypress/composables/credentialsComposables.ts +114 -0
  42. cypress/composables/executions.ts +46 -0
  43. cypress/composables/featureFlags.ts +12 -0
  44. cypress/composables/folders.ts +553 -0
  45. cypress/composables/logs.ts +91 -0
  46. cypress/composables/modals/chat-modal.ts +40 -0
  47. cypress/composables/modals/credential-modal.ts +62 -0
  48. cypress/composables/modals/save-changes-modal.ts +13 -0
  49. cypress/composables/modals/workflow-credential-setup-modal.ts +13 -0
  50. cypress/composables/ndv.ts +347 -0
.bundlemonrc.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "baseDir": "packages/frontend/editor-ui/dist",
3
+ "defaultCompression": "gzip",
4
+ "reportOutput": [
5
+ [
6
+ "github",
7
+ {
8
+ "checkRun": true,
9
+ "commitStatus": "off",
10
+ "prComment": true
11
+ }
12
+ ]
13
+ ],
14
+ "files": [
15
+ {
16
+ "path": "*.wasm",
17
+ "friendlyName": "WASM Dependencies"
18
+ }
19
+ ],
20
+ "groups": [
21
+ {
22
+ "groupName": "Editor UI - Total JS Size",
23
+ "path": "**/*.js"
24
+ },
25
+ {
26
+ "groupName": "Editor UI - Total CSS Size",
27
+ "path": "**/*.css"
28
+ }
29
+ ]
30
+ }
.cursorindexingignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+
2
+ # Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
3
+ .specstory/**
.devcontainer/Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM n8nio/base:22
2
+
3
+ RUN apk add --no-cache --update openssh sudo shadow bash
4
+ RUN echo node ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/node && chmod 0440 /etc/sudoers.d/node
5
+ RUN mkdir /workspaces && chown node:node /workspaces
6
+ USER node
7
+ RUN mkdir -p ~/.pnpm-store && pnpm config set store-dir ~/.pnpm-store --global
.devcontainer/devcontainer.json ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "n8n",
3
+ "dockerComposeFile": "docker-compose.yml",
4
+ "service": "n8n",
5
+ "workspaceFolder": "/workspaces",
6
+ "mounts": [
7
+ "type=bind,source=${localWorkspaceFolder},target=/workspaces,consistency=cached",
8
+ "type=bind,source=${localEnv:HOME}/.ssh,target=/home/node/.ssh,consistency=cached",
9
+ "type=bind,source=${localEnv:HOME}/.n8n,target=/home/node/.n8n,consistency=cached"
10
+ ],
11
+ "forwardPorts": [8080, 5678],
12
+ "postCreateCommand": "corepack prepare --activate && pnpm install",
13
+ "postAttachCommand": "pnpm build",
14
+ "customizations": {
15
+ "codespaces": {
16
+ "openFiles": ["CONTRIBUTING.md"]
17
+ }
18
+ }
19
+ }
.devcontainer/docker-compose.yml ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ volumes:
2
+ postgres-data:
3
+
4
+ services:
5
+ postgres:
6
+ image: postgres:16-alpine
7
+ restart: unless-stopped
8
+ volumes:
9
+ - postgres-data:/var/lib/postgresql/data
10
+ environment:
11
+ - POSTGRES_DB=n8n
12
+ - POSTGRES_PASSWORD=password
13
+
14
+ n8n:
15
+ build:
16
+ context: .
17
+ dockerfile: Dockerfile
18
+ volumes:
19
+ - ..:/workspaces:cached
20
+ command: sleep infinity
21
+ environment:
22
+ DB_POSTGRESDB_HOST: postgres
23
+ DB_TYPE: postgresdb
24
+ DB_POSTGRESDB_PASSWORD: password
.dockerignore ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ **/*.md
2
+ **/.env
3
+ .cache
4
+ assets
5
+ node_modules
6
+ packages/node-dev
7
+ packages/**/node_modules
8
+ packages/**/dist
9
+ packages/**/.turbo
10
+ packages/**/*.test.*
11
+ .git
12
+ .github
13
+ !.github/scripts
14
+ *.tsbuildinfo
15
+ packages/cli/dist/**/e2e.*
16
+ docker/compose
17
+ docker/**/Dockerfile
18
+ .vscode
19
+ cypress
20
+ test-workflows
.editorconfig ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ indent_style = tab
6
+ indent_size = 2
7
+ end_of_line = lf
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [package.json]
12
+ indent_style = space
13
+ indent_size = 2
14
+
15
+ [*.yml]
16
+ indent_style = space
17
+ indent_size = 2
18
+
19
+ [*.ts]
20
+ quote_type = single
.env.enterprise-mock ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # N8N企业版功能模拟配置
2
+ # 仅用于开发和测试环境
3
+
4
+ # 启用企业版功能模拟
5
+ N8N_ENTERPRISE_MOCK=true
6
+
7
+ # 设置为开发环境
8
+ NODE_ENV=development
9
+
10
+ # 禁用许可证服务器验证(可选)
11
+ N8N_LICENSE_SERVER_URL=
12
+
13
+ # 许可证相关配置(模拟)
14
+ N8N_LICENSE_AUTO_RENEW_ENABLED=false
15
+ N8N_LICENSE_CERT=
16
+
17
+ # 用户管理相关
18
+ N8N_USER_MANAGEMENT_JWT_SECRET=mock-jwt-secret
19
+ N8N_USER_MANAGEMENT_DISABLED=false
20
+
21
+ # 其他企业功能配置
22
+ N8N_SAML_ENABLED=true
23
+ N8N_LDAP_ENABLED=true
24
+ N8N_LOG_STREAMING_ENABLED=true
25
+ N8N_VARIABLES_ENABLED=true
26
+ N8N_SOURCE_CONTROL_ENABLED=true
27
+ N8N_EXTERNAL_SECRETS_ENABLED=true
28
+ N8N_WORKFLOW_HISTORY_ENABLED=true
29
+
30
+ # 数据库配置(根据需要调整)
31
+ DB_TYPE=sqlite
32
+ DB_SQLITE_DATABASE=database.sqlite
33
+
34
+ # 禁用遥测(可选)
35
+ N8N_DIAGNOSTICS_ENABLED=false
36
+ N8N_VERSION_NOTIFICATIONS_ENABLED=false
37
+
38
+ # 日志级别
39
+ N8N_LOG_LEVEL=debug
40
+
41
+ # 启用调试模式
42
+ N8N_DEBUG=true
43
+
44
+ # AI Assistant配置 (模拟)
45
+ N8N_AI_ASSISTANT_BASE_URL=https://api.n8n.io
.env.example ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # n8n 中文版 - Hugging Face 部署环境变量示例
2
+ # 复制此文件并重命名为 .env,然后填入您的实际值
3
+
4
+ # ===========================================
5
+ # 数据库配置 (必需)
6
+ # ===========================================
7
+ DB_TYPE=postgresdb
8
+ DB_POSTGRESDB_HOST=your-postgres-host.com
9
+ DB_POSTGRESDB_PORT=5432
10
+ DB_POSTGRESDB_DATABASE=n8n
11
+ DB_POSTGRESDB_USER=your-username
12
+ DB_POSTGRESDB_PASSWORD=your-password
13
+
14
+ # ===========================================
15
+ # n8n 基础配置
16
+ # ===========================================
17
+ N8N_PORT=7860
18
+ N8N_DEFAULT_LOCALE=zh-CN
19
+ N8N_ENTERPRISE_MOCK=true
20
+ NODE_ENV=production
21
+
22
+ # Webhook URL - 替换为您的实际 Space URL
23
+ WEBHOOK_URL=https://your-space-name-n8n-chs.hf.space
24
+
25
+ # ===========================================
26
+ # 安全配置 (强烈推荐)
27
+ # ===========================================
28
+ # 生成32字符的随机字符串作为加密密钥
29
+ N8N_ENCRYPTION_KEY=your-32-character-encryption-key-here
30
+
31
+ # JWT 密钥用于用户认证
32
+ N8N_USER_MANAGEMENT_JWT_SECRET=your-jwt-secret-here
33
+
34
+ # ===========================================
35
+ # 功能配置 (可选)
36
+ # ===========================================
37
+ # 禁用遥测和分析
38
+ N8N_METRICS=false
39
+ N8N_DIAGNOSTICS_ENABLED=false
40
+ N8N_VERSION_NOTIFICATIONS_ENABLED=false
41
+
42
+ # 模板和引导流程
43
+ N8N_TEMPLATES_ENABLED=true
44
+ N8N_ONBOARDING_FLOW_DISABLED=true
45
+ N8N_HIRING_BANNER_ENABLED=false
46
+
47
+ # Cookie 安全设置
48
+ N8N_SECURE_COOKIE=false
49
+
50
+ # ===========================================
51
+ # 高级配置 (可选)
52
+ # ===========================================
53
+ # 执行超时设置 (毫秒)
54
+ EXECUTIONS_TIMEOUT=3600000
55
+ EXECUTIONS_TIMEOUT_MAX=3600000
56
+
57
+ # 工作流执行模式
58
+ EXECUTIONS_PROCESS=main
59
+
60
+ # 日志级别
61
+ N8N_LOG_LEVEL=info
62
+
63
+ # ===========================================
64
+ # Hugging Face 特定配置
65
+ # ===========================================
66
+ # Space ID (自动检测,通常不需要手动设置)
67
+ # SPACE_ID=your-space-name-n8n-chs
68
+
69
+ # ===========================================
70
+ # 数据库连接示例
71
+ # ===========================================
72
+ # Supabase 示例:
73
+ # DB_POSTGRESDB_HOST=db.your-project.supabase.co
74
+ # DB_POSTGRESDB_PORT=5432
75
+ # DB_POSTGRESDB_DATABASE=postgres
76
+ # DB_POSTGRESDB_USER=postgres
77
+ # DB_POSTGRESDB_PASSWORD=your-supabase-password
78
+
79
+ # ElephantSQL 示例:
80
+ # DB_POSTGRESDB_HOST=your-server.db.elephantsql.com
81
+ # DB_POSTGRESDB_PORT=5432
82
+ # DB_POSTGRESDB_DATABASE=your-database
83
+ # DB_POSTGRESDB_USER=your-user
84
+ # DB_POSTGRESDB_PASSWORD=your-password
85
+
86
+ # Railway 示例:
87
+ # DB_POSTGRESDB_HOST=containers-us-west-xxx.railway.app
88
+ # DB_POSTGRESDB_PORT=6543
89
+ # DB_POSTGRESDB_DATABASE=railway
90
+ # DB_POSTGRESDB_USER=postgres
91
+ # DB_POSTGRESDB_PASSWORD=your-railway-password
.git-blame-ignore-revs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Commits of large-scale changes to exclude from `git blame` results
2
+
3
+ # Set up linting and formatting (#2120)
4
+
5
+ 56c4c6991fb21ba4b7bdcd22c929f63cc1d1defe
6
+
7
+ # refactor(editor): Apply Prettier (no-changelog) #4920
8
+
9
+ 5ca2148c7ed06c90f999508928b7a51f9ac7a788
10
+
11
+ # refactor: Run lintfix (no-changelog) (#7537)
12
+
13
+ 62c096710fab2f7e886518abdbded34b55e93f62
14
+
15
+ # refactor: Move test files alongside tested files (#11504)
16
+
17
+ 7e58fc4fec468aca0b45d5bfe6150e1af632acbc
18
+ f32b13c6ed078be042a735bc8621f27e00dc3116
.gitignore ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ node_modules
2
+ .DS_Store
3
+ .tmp
4
+ tmp
5
+ dist
6
+ coverage
7
+ npm-debug.log*
8
+ yarn.lock
9
+ google-generated-credentials.json
10
+ _START_PACKAGE
11
+ .env
12
+ .vscode/*
13
+ !.vscode/extensions.json
14
+ !.vscode/settings.default.json
15
+ .idea
16
+ nodelinter.config.json
17
+ **/package-lock.json
18
+ packages/**/.turbo
19
+ .turbo
20
+ *.tsbuildinfo
21
+ *.swp
22
+ CHANGELOG-*.md
23
+ *.mdx
24
+ build-storybook.log
25
+ *.junit.xml
26
+ junit.xml
27
+ test-results.json
28
+ *.0x
29
+ .specstory/*
.npmignore ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ dist/test
2
+ dist/**/*.{js.map}
3
+
4
+ .DS_Store
5
+
6
+ # local env files
7
+ .env.local
8
+ .env.*.local
9
+
10
+ # Log files
11
+ yarn-debug.log*
12
+ yarn-error.log*
13
+
14
+ # Editor directories and files
15
+ .idea
16
+ .vscode
17
+ *.suo
18
+ *.ntvs*
19
+ *.njsproj
20
+ *.sln
21
+ *.sw*
22
+
23
+ .editorconfig
24
+ .eslintrc.js
25
+ tsconfig.json
26
+
27
+ .turbo
28
+ *.tsbuildinfo
.npmrc ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ audit = false
2
+ fund = false
3
+ update-notifier = false
4
+ auto-install-peers = true
5
+ strict-peer-dependencies = false
6
+ prefer-workspace-packages = true
7
+ link-workspace-packages = deep
8
+ hoist = true
9
+ shamefully-hoist = true
10
+ hoist-workspace-packages = false
11
+ loglevel = warn
12
+ package-manager-strict=false
13
+ # https://github.com/pnpm/pnpm/issues/7024
14
+ package-import-method=clone-or-copy
.prettierignore ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ coverage
2
+ dist
3
+ package.json
4
+ pnpm-lock.yaml
5
+ packages/frontend/editor-ui/index.html
6
+ packages/nodes-base/nodes/**/test
7
+ packages/cli/templates/form-trigger.handlebars
8
+ packages/cli/templates/form-trigger-completion.handlebars
9
+ packages/cli/templates/form-trigger-409.handlebars
10
+ packages/cli/templates/form-trigger-404.handlebars
11
+ cypress/fixtures
12
+ CHANGELOG.md
13
+ .github/pull_request_template.md
14
+ # Ignored for now
15
+ **/*.md
16
+ # Handled by biome
17
+ **/*.ts
18
+ **/*.js
19
+ **/*.json
20
+ **/*.jsonc
21
+
22
+ # Auto-generated
23
+ **/components.d.ts
.prettierrc.js ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ /**
3
+ * https://prettier.io/docs/en/options.html#semicolons
4
+ */
5
+ semi: true,
6
+
7
+ /**
8
+ * https://prettier.io/docs/en/options.html#trailing-commas
9
+ */
10
+ trailingComma: 'all',
11
+
12
+ /**
13
+ * https://prettier.io/docs/en/options.html#bracket-spacing
14
+ */
15
+ bracketSpacing: true,
16
+
17
+ /**
18
+ * https://prettier.io/docs/en/options.html#tabs
19
+ */
20
+ useTabs: true,
21
+
22
+ /**
23
+ * https://prettier.io/docs/en/options.html#tab-width
24
+ */
25
+ tabWidth: 2,
26
+
27
+ /**
28
+ * https://prettier.io/docs/en/options.html#arrow-function-parentheses
29
+ */
30
+ arrowParens: 'always',
31
+
32
+ /**
33
+ * https://prettier.io/docs/en/options.html#quotes
34
+ */
35
+ singleQuote: true,
36
+
37
+ /**
38
+ * https://prettier.io/docs/en/options.html#quote-props
39
+ */
40
+ quoteProps: 'as-needed',
41
+
42
+ /**
43
+ * https://prettier.io/docs/en/options.html#end-of-line
44
+ */
45
+ endOfLine: 'lf',
46
+
47
+ /**
48
+ * https://prettier.io/docs/en/options.html#print-width
49
+ */
50
+ printWidth: 100,
51
+ };
.vscode/extensions.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "recommendations": [
3
+ "biomejs.biome",
4
+ "streetsidesoftware.code-spell-checker",
5
+ "dangmai.workspace-default-settings",
6
+ "dbaeumer.vscode-eslint",
7
+ "EditorConfig.EditorConfig",
8
+ "esbenp.prettier-vscode",
9
+ "mjmlio.vscode-mjml",
10
+ "ryanluker.vscode-coverage-gutters",
11
+ "Vue.volar",
12
+ "vitest.explorer"
13
+ ]
14
+ }
.vscode/settings.default.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "editor.defaultFormatter": "esbenp.prettier-vscode",
3
+ "editor.formatOnSave": true,
4
+ "[javascript]": {
5
+ "editor.defaultFormatter": "biomejs.biome"
6
+ },
7
+ "[typescript]": {
8
+ "editor.defaultFormatter": "biomejs.biome"
9
+ },
10
+ "[json]": {
11
+ "editor.defaultFormatter": "biomejs.biome"
12
+ },
13
+ "[jsonc]": {
14
+ "editor.defaultFormatter": "biomejs.biome"
15
+ },
16
+ "editor.codeActionsOnSave": {
17
+ "quickfix.biome": "explicit",
18
+ "source.organizeImports.biome": "never"
19
+ },
20
+ "search.exclude": {
21
+ "node_modules": true,
22
+ "dist": true,
23
+ "pnpm-lock.yaml": true,
24
+ "**/*.snapshot.json": true,
25
+ "test-workflows": true
26
+ },
27
+ "typescript.format.enable": false,
28
+ "typescript.tsdk": "node_modules/typescript/lib",
29
+ "workspace-default-settings.runOnActivation": true,
30
+ "prettier.prettierPath": "node_modules/prettier/index.cjs",
31
+ "eslint.probe": ["javascript", "typescript", "vue"],
32
+ "eslint.workingDirectories": [
33
+ {
34
+ "mode": "auto"
35
+ }
36
+ ]
37
+ }
CHANGELOG.md ADDED
The diff for this file is too large to render. See raw diff
 
CODE_OF_CONDUCT.md ADDED
@@ -0,0 +1,76 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, sex characteristics, gender identity and expression,
9
+ level of experience, education, socio-economic status, nationality, personal
10
+ appearance, race, religion, or sexual identity and orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ - Using welcoming and inclusive language
18
+ - Being respectful of differing viewpoints and experiences
19
+ - Gracefully accepting constructive criticism
20
+ - Focusing on what is best for the community
21
+ - Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ - The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ - Trolling, insulting/derogatory comments, and personal or political attacks
28
+ - Public or private harassment
29
+ - Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ - Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at jan@n8n.io. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72
+
73
+ [homepage]: https://www.contributor-covenant.org
74
+
75
+ For answers to common questions about this code of conduct, see
76
+ https://www.contributor-covenant.org/faq
CONTRIBUTING.md ADDED
@@ -0,0 +1,315 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Contributing to n8n
2
+
3
+ Great that you are here and you want to contribute to n8n
4
+
5
+ ## Contents
6
+
7
+ - [Contributing to n8n](#contributing-to-n8n)
8
+ - [Contents](#contents)
9
+ - [Code of conduct](#code-of-conduct)
10
+ - [Directory structure](#directory-structure)
11
+ - [Development setup](#development-setup)
12
+ - [Dev Container](#dev-container)
13
+ - [Requirements](#requirements)
14
+ - [Node.js](#nodejs)
15
+ - [pnpm](#pnpm)
16
+ - [pnpm workspaces](#pnpm-workspaces)
17
+ - [corepack](#corepack)
18
+ - [Build tools](#build-tools)
19
+ - [Actual n8n setup](#actual-n8n-setup)
20
+ - [Start](#start)
21
+ - [Development cycle](#development-cycle)
22
+ - [Community PR Guidelines](#community-pr-guidelines)
23
+ - [**1. Change Request/Comment**](#1-change-requestcomment)
24
+ - [**2. General Requirements**](#2-general-requirements)
25
+ - [**3. PR Specific Requirements**](#3-pr-specific-requirements)
26
+ - [**4. Workflow Summary for Non-Compliant PRs**](#4-workflow-summary-for-non-compliant-prs)
27
+ - [Test suite](#test-suite)
28
+ - [Unit tests](#unit-tests)
29
+ - [Code Coverage](#code-coverage)
30
+ - [E2E tests](#e2e-tests)
31
+ - [Releasing](#releasing)
32
+ - [Create custom nodes](#create-custom-nodes)
33
+ - [Extend documentation](#extend-documentation)
34
+ - [Contribute workflow templates](#contribute-workflow-templates)
35
+ - [Contributor License Agreement](#contributor-license-agreement)
36
+
37
+ ## Code of conduct
38
+
39
+ This project and everyone participating in it are governed by the Code of
40
+ Conduct which can be found in the file [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
41
+ By participating, you are expected to uphold this code. Please report
42
+ unacceptable behavior to jan@n8n.io.
43
+
44
+ ## Directory structure
45
+
46
+ n8n is split up in different modules which are all in a single mono repository.
47
+
48
+ The most important directories:
49
+
50
+ - [/docker/images](/docker/images) - Dockerfiles to create n8n containers
51
+ - [/packages](/packages) - The different n8n modules
52
+ - [/packages/cli](/packages/cli) - CLI code to run front- & backend
53
+ - [/packages/core](/packages/core) - Core code which handles workflow
54
+ execution, active webhooks and
55
+ workflows. **Contact n8n before
56
+ starting on any changes here**
57
+ - [/packages/frontend/@n8n/design-system](/packages/design-system) - Vue frontend components
58
+ - [/packages/frontend/editor-ui](/packages/editor-ui) - Vue frontend workflow editor
59
+ - [/packages/node-dev](/packages/node-dev) - CLI to create new n8n-nodes
60
+ - [/packages/nodes-base](/packages/nodes-base) - Base n8n nodes
61
+ - [/packages/workflow](/packages/workflow) - Workflow code with interfaces which
62
+ get used by front- & backend
63
+
64
+ ## Development setup
65
+
66
+ If you want to change or extend n8n you have to make sure that all the needed
67
+ dependencies are installed and the packages get linked correctly. Here's a short guide on how that can be done:
68
+
69
+ ### Dev Container
70
+
71
+ If you already have VS Code and Docker installed, you can click [here](https://vscode.dev/redirect?url=vscode://ms-vscode-remote.remote-containers/cloneInVolume?url=https://github.com/n8n-io/n8n) to get started. Clicking these links will cause VS Code to automatically install the Dev Containers extension if needed, clone the source code into a container volume, and spin up a dev container for use.
72
+
73
+ ### Requirements
74
+
75
+ #### Node.js
76
+
77
+ [Node.js](https://nodejs.org/en/) version 22.16 or newer is required for development purposes.
78
+
79
+ #### pnpm
80
+
81
+ [pnpm](https://pnpm.io/) version 10.2 or newer is required for development purposes. We recommend installing it with [corepack](#corepack).
82
+
83
+ ##### pnpm workspaces
84
+
85
+ n8n is split up into different modules which are all in a single mono repository.
86
+ To facilitate the module management, [pnpm workspaces](https://pnpm.io/workspaces) are used.
87
+ This automatically sets up file-links between modules which depend on each other.
88
+
89
+ #### corepack
90
+
91
+ We recommend enabling [Node.js corepack](https://nodejs.org/docs/latest-v16.x/api/corepack.html) with `corepack enable`.
92
+
93
+ You can install the correct version of pnpm using `corepack prepare --activate`.
94
+
95
+ **IMPORTANT**: If you have installed Node.js via homebrew, you'll need to run `brew install corepack`, since homebrew explicitly removes `npm` and `corepack` from [the `node` formula](https://github.com/Homebrew/homebrew-core/blob/master/Formula/node.rb#L66).
96
+
97
+ **IMPORTANT**: If you are on windows, you'd need to run `corepack enable` and `corepack prepare --activate` in a terminal as an administrator.
98
+
99
+ #### Build tools
100
+
101
+ The packages which n8n uses depend on a few build tools:
102
+
103
+ Debian/Ubuntu:
104
+
105
+ ```
106
+ apt-get install -y build-essential python
107
+ ```
108
+
109
+ CentOS:
110
+
111
+ ```
112
+ yum install gcc gcc-c++ make
113
+ ```
114
+
115
+ Windows:
116
+
117
+ ```
118
+ npm add -g windows-build-tools
119
+ ```
120
+
121
+ MacOS:
122
+
123
+ No additional packages required.
124
+
125
+ ### Actual n8n setup
126
+
127
+ > **IMPORTANT**: All the steps below have to get executed at least once to get the development setup up and running!
128
+
129
+ Now that everything n8n requires to run is installed, the actual n8n code can be
130
+ checked out and set up:
131
+
132
+ 1. [Fork](https://guides.github.com/activities/forking/#fork) the n8n repository.
133
+
134
+ 2. Clone your forked repository:
135
+
136
+ ```
137
+ git clone https://github.com/<your_github_username>/n8n.git
138
+ ```
139
+
140
+ 3. Go into repository folder:
141
+
142
+ ```
143
+ cd n8n
144
+ ```
145
+
146
+ 4. Add the original n8n repository as `upstream` to your forked repository:
147
+
148
+ ```
149
+ git remote add upstream https://github.com/n8n-io/n8n.git
150
+ ```
151
+
152
+ 5. Install all dependencies of all modules and link them together:
153
+
154
+ ```
155
+ pnpm install
156
+ ```
157
+
158
+ 6. Build all the code:
159
+ ```
160
+ pnpm build
161
+ ```
162
+
163
+ ### Start
164
+
165
+ To start n8n execute:
166
+
167
+ ```
168
+ pnpm start
169
+ ```
170
+
171
+ To start n8n with tunnel:
172
+
173
+ ```
174
+ ./packages/cli/bin/n8n start --tunnel
175
+ ```
176
+
177
+ ## Development cycle
178
+
179
+ While iterating on n8n modules code, you can run `pnpm dev`. It will then
180
+ automatically build your code, restart the backend and refresh the frontend
181
+ (editor-ui) on every change you make.
182
+
183
+ 1. Start n8n in development mode:
184
+ ```
185
+ pnpm dev
186
+ ```
187
+ 1. Hack, hack, hack
188
+ 1. Check if everything still runs in production mode:
189
+ ```
190
+ pnpm build
191
+ pnpm start
192
+ ```
193
+ 1. Create tests
194
+ 1. Run all [tests](#test-suite):
195
+ ```
196
+ pnpm test
197
+ ```
198
+ 1. Commit code and [create a pull request](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork)
199
+
200
+ ---
201
+
202
+ ### Community PR Guidelines
203
+
204
+ #### **1. Change Request/Comment**
205
+
206
+ Please address the requested changes or provide feedback within 14 days. If there is no response or updates to the pull request during this time, it will be automatically closed. The PR can be reopened once the requested changes are applied.
207
+
208
+ #### **2. General Requirements**
209
+
210
+ - **Follow the Style Guide:**
211
+ - Ensure your code adheres to n8n's coding standards and conventions (e.g., formatting, naming, indentation). Use linting tools where applicable.
212
+ - **TypeScript Compliance:**
213
+ - Do not use `ts-ignore` .
214
+ - Ensure code adheres to TypeScript rules.
215
+ - **Avoid Repetitive Code:**
216
+ - Reuse existing components, parameters, and logic wherever possible instead of redefining or duplicating them.
217
+ - For nodes: Use the same parameter across multiple operations rather than defining a new parameter for each operation (if applicable).
218
+ - **Testing Requirements:**
219
+ - PRs **must include tests**:
220
+ - Unit tests
221
+ - Workflow tests for nodes (example [here](https://github.com/n8n-io/n8n/tree/master/packages/nodes-base/nodes/Switch/V3/test))
222
+ - UI tests (if applicable)
223
+ - **Typos:**
224
+ - Use a spell-checking tool, such as [**Code Spell Checker**](https://marketplace.visualstudio.com/items?itemName=streetsidesoftware.code-spell-checker), to avoid typos.
225
+
226
+ #### **3. PR Specific Requirements**
227
+
228
+ - **Small PRs Only:**
229
+ - Focus on a single feature or fix per PR.
230
+ - **Naming Convention:**
231
+ - Follow [n8n's PR Title Conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md#L36).
232
+ - **New Nodes:**
233
+ - PRs that introduce new nodes will be **auto-closed** unless they are explicitly requested by the n8n team and aligned with an agreed project scope. However, you can still explore [building your own nodes](https://docs.n8n.io/integrations/creating-nodes/) , as n8n offers the flexibility to create your own custom nodes.
234
+ - **Typo-Only PRs:**
235
+ - Typos are not sufficient justification for a PR and will be rejected.
236
+
237
+ #### **4. Workflow Summary for Non-Compliant PRs**
238
+
239
+ - **No Tests:** If tests are not provided, the PR will be auto-closed after **14 days**.
240
+ - **Non-Small PRs:** Large or multifaceted PRs will be returned for segmentation.
241
+ - **New Nodes/Typo PRs:** Automatically rejected if not aligned with project scope or guidelines.
242
+
243
+ ---
244
+
245
+ ### Test suite
246
+
247
+ #### Unit tests
248
+
249
+ Unit tests can be started via:
250
+
251
+ ```
252
+ pnpm test
253
+ ```
254
+
255
+ If that gets executed in one of the package folders it will only run the tests
256
+ of this package. If it gets executed in the n8n-root folder it will run all
257
+ tests of all packages.
258
+
259
+ If you made a change which requires an update on a `.test.ts.snap` file, pass `-u` to the command to run tests or press `u` in watch mode.
260
+
261
+ #### Code Coverage
262
+ We track coverage for all our code on [Codecov](https://app.codecov.io/gh/n8n-io/n8n).
263
+ But when you are working on tests locally, we recommend running your tests with env variable `COVERAGE_ENABLED` set to `true`. You can then view the code coverage in the `coverage` folder, or you can use [this VSCode extension](https://marketplace.visualstudio.com/items?itemName=ryanluker.vscode-coverage-gutters) to visualize the coverage directly in VSCode.
264
+
265
+ #### E2E tests
266
+
267
+ ⚠️ You have to run `pnpm cypress:install` to install cypress before running the tests for the first time and to update cypress.
268
+
269
+ E2E tests can be started via one of the following commands:
270
+
271
+ - `pnpm test:e2e:ui`: Start n8n and run e2e tests interactively using built UI code. Does not react to code changes (i.e. runs `pnpm start` and `cypress open`)
272
+ - `pnpm test:e2e:dev`: Start n8n in development mode and run e2e tests interactively. Reacts to code changes (i.e. runs `pnpm dev` and `cypress open`)
273
+ - `pnpm test:e2e:all`: Start n8n and run e2e tests headless (i.e. runs `pnpm start` and `cypress run --headless`)
274
+
275
+ ⚠️ Remember to stop your dev server before. Otherwise port binding will fail.
276
+
277
+ ## Releasing
278
+
279
+ To start a release, trigger [this workflow](https://github.com/n8n-io/n8n/actions/workflows/release-create-pr.yml) with the SemVer release type, and select a branch to cut this release from. This workflow will then:
280
+
281
+ 1. Bump versions of packages that have changed or have dependencies that have changed
282
+ 2. Update the Changelog
283
+ 3. Create a new branch called `release/${VERSION}`, and
284
+ 4. Create a new pull-request to track any further changes that need to be included in this release
285
+
286
+ Once ready to release, simply merge the pull-request.
287
+ This triggers [another workflow](https://github.com/n8n-io/n8n/actions/workflows/release-publish.yml), that will:
288
+
289
+ 1. Build and publish the packages that have a new version in this release
290
+ 2. Create a new tag, and GitHub release from squashed release commit
291
+ 3. Merge the squashed release commit back into `master`
292
+
293
+ ## Create custom nodes
294
+
295
+ Learn about [building nodes](https://docs.n8n.io/integrations/creating-nodes/) to create custom nodes for n8n. You can create community nodes and make them available using [npm](https://www.npmjs.com/).
296
+
297
+ ## Extend documentation
298
+
299
+ The repository for the n8n documentation on [docs.n8n.io](https://docs.n8n.io) can be found [here](https://github.com/n8n-io/n8n-docs).
300
+
301
+ ## Contribute workflow templates
302
+
303
+ You can submit your workflows to n8n's template library.
304
+
305
+ n8n is working on a creator program, and developing a marketplace of templates. This is an ongoing project, and details are likely to change.
306
+
307
+ Refer to [n8n Creator hub](https://www.notion.so/n8n/n8n-Creator-hub-7bd2cbe0fce0449198ecb23ff4a2f76f) for information on how to submit templates and become a creator.
308
+
309
+ ## Contributor License Agreement
310
+
311
+ That we do not have any potential problems later it is sadly necessary to sign a [Contributor License Agreement](CONTRIBUTOR_LICENSE_AGREEMENT.md). That can be done literally with the push of a button.
312
+
313
+ We used the most simple one that exists. It is from [Indie Open Source](https://indieopensource.com/forms/cla) which uses plain English and is literally only a few lines long.
314
+
315
+ Once a pull request is opened, an automated bot will promptly leave a comment requesting the agreement to be signed. The pull request can only be merged once the signature is obtained.
CONTRIBUTOR_LICENSE_AGREEMENT.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ # n8n Contributor License Agreement
2
+
3
+ I give n8n permission to license my contributions on any terms they like. I am giving them this license in order to make it possible for them to accept my contributions into their project.
4
+
5
+ **_As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim._**
DEPLOYMENT-SUMMARY.md ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # n8n 中文版 - Hugging Face 部署总结
2
+
3
+ ## 📦 已创建的部署文件
4
+
5
+ 我已经为您创建了以下文件,用于在 Hugging Face Spaces 上部署 n8n 中文版:
6
+
7
+ ### 核心部署文件
8
+ - `Dockerfile.hf` - Hugging Face 专用 Dockerfile
9
+ - `start-hf.sh` - 启动脚本
10
+ - `app.py` - Python 启动器(Hugging Face 兼容)
11
+ - `requirements.txt` - Python 依赖文件
12
+
13
+ ### 配置和文档
14
+ - `README-HUGGINGFACE.md` - 详细部署指南
15
+ - `.env.example` - 环境变量配置示例
16
+ - `deploy-to-hf.sh` - 自动化部署准备脚本
17
+ - `DEPLOYMENT-SUMMARY.md` - 本文件
18
+
19
+ ## 🚀 快速部署步骤
20
+
21
+ ### 1. 准备数据库
22
+ 选择一个 PostgreSQL 数据库服务:
23
+ - **Supabase** (推荐): https://supabase.com
24
+ - **ElephantSQL**: https://www.elephantsql.com
25
+ - **Railway**: https://railway.app
26
+ - **Aiven**: https://aiven.io
27
+
28
+ ### 2. 创建 Hugging Face Space
29
+ 1. 访问 https://huggingface.co/spaces
30
+ 2. 点击 "Create new Space"
31
+ 3. 选择 "Docker" SDK
32
+ 4. 设置 Space 名称
33
+ 5. 选择硬件配置(推荐 CPU basic 或更高)
34
+
35
+ ### 3. 配置环境变量
36
+ 在 Space 设置中添加以下环境变量:
37
+
38
+ ```bash
39
+ # 数据库配置(必需)
40
+ DB_TYPE=postgresdb
41
+ DB_POSTGRESDB_HOST=your-postgres-host.com
42
+ DB_POSTGRESDB_PORT=5432
43
+ DB_POSTGRESDB_DATABASE=your-database-name
44
+ DB_POSTGRESDB_USER=your-username
45
+ DB_POSTGRESDB_PASSWORD=your-password
46
+
47
+ # n8n 配置
48
+ N8N_DEFAULT_LOCALE=zh-CN
49
+ N8N_ENTERPRISE_MOCK=true
50
+ N8N_PORT=7860
51
+ WEBHOOK_URL=https://your-space-name.hf.space
52
+
53
+ # 安全配置(推荐)
54
+ N8N_ENCRYPTION_KEY=your-32-character-encryption-key
55
+ N8N_USER_MANAGEMENT_JWT_SECRET=your-jwt-secret
56
+ ```
57
+
58
+ ### 4. 上传文件
59
+ 将以下文件上传到您的 Hugging Face Space:
60
+
61
+ **方法一:手动上传**
62
+ 1. 将 `Dockerfile.hf` 重命名为 `Dockerfile`
63
+ 2. 上传所有项目文件到 Space
64
+
65
+ **方法二:使用部署脚本**
66
+ 1. 运行 `./deploy-to-hf.sh`(Linux/Mac)
67
+ 2. 上传生成的 `deploy-hf` 目录中的文件
68
+
69
+ ### 5. 等待部署
70
+ - Space 会自动构建 Docker 镜像
71
+ - 查看构建日志确认无错误
72
+ - 构建完成后访问您的 Space URL
73
+
74
+ ## 🔧 重要配置说明
75
+
76
+ ### 数据库连接
77
+ - 必须使用外部 PostgreSQL 数据库
78
+ - 不支持 SQLite(Hugging Face 环境限制)
79
+ - 确保数据库允许外部连接
80
+
81
+ ### 端口配置
82
+ - Hugging Face Spaces 使用端口 7860
83
+ - 已在 Dockerfile 中配置正确
84
+
85
+ ### Webhook 配置
86
+ - 自动检测 Space URL 设置 Webhook
87
+ - 也可以手动设置 `WEBHOOK_URL` 环境变量
88
+
89
+ ### 安全设置
90
+ - 强烈建议设置 `N8N_ENCRYPTION_KEY`
91
+ - 设置 `N8N_USER_MANAGEMENT_JWT_SECRET` 用于用户认证
92
+ - 密钥应为随机生成的强密码
93
+
94
+ ## 🛠️ 故障排除
95
+
96
+ ### 常见问题
97
+
98
+ **构建失败**
99
+ - 检查 Dockerfile 语法
100
+ - 确认所有文件都已上传
101
+ - 查看构建日志中的错误信息
102
+
103
+ **数据库连接失败**
104
+ - 验证数据库环境变量
105
+ - 确认数据库服务器在线
106
+ - 检查防火墙和网络设置
107
+
108
+ **应用无法访问**
109
+ - 确认端口设置为 7860
110
+ - 检查 Space 的公开访问权限
111
+ - 查看运行时日志
112
+
113
+ ### 日志查看
114
+ 在 Hugging Face Space 界面:
115
+ 1. 点击 "Logs" 标签
116
+ 2. 查看构建日志和运行时日志
117
+ 3. 寻找错误信息和警告
118
+
119
+ ## 📋 部署检查清单
120
+
121
+ - [ ] PostgreSQL 数据库已准备
122
+ - [ ] Hugging Face Space 已创建
123
+ - [ ] 环境变量已配置
124
+ - [ ] 项目文件已上传
125
+ - [ ] Dockerfile 已正确命名
126
+ - [ ] 构建成功完成
127
+ - [ ] 应用可以访问
128
+ - [ ] 数据库连接正常
129
+ - [ ] 管理员账户已创建
130
+
131
+ ## 🎯 部署后操作
132
+
133
+ 1. **首次访问**
134
+ - 访问您的 Space URL
135
+ - 创建管理员账户
136
+ - 设置基本配置
137
+
138
+ 2. **测试功能**
139
+ - 创建简单工作流
140
+ - 测试 Webhook 功能
141
+ - 验证数据库存储
142
+
143
+ 3. **安全配置**
144
+ - 更改默认密码
145
+ - 配置用户权限
146
+ - 启用必要的安全功能
147
+
148
+ ## 📞 获取帮助
149
+
150
+ 如果遇到问题:
151
+ 1. 查看 `README-HUGGINGFACE.md` 详细文档
152
+ 2. 检查 Space 的构建和运行日志
153
+ 3. 参考 n8n 官方文档
154
+ 4. 在项目 GitHub 仓库提交 Issue
155
+
156
+ ---
157
+
158
+ **祝您部署成功!🎉**
ENTERPRISE-MOCK-README.md ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # N8N 企业版功能模拟
2
+
3
+ 此文档说明如何在n8n项目中启用企业版功能模拟,用于开发和测试。
4
+
5
+ ## ⚠️ 重要警告
6
+
7
+ **此模拟仅用于开发和测试环境!请勿在生产环境中使用!**
8
+
9
+ ## 🚀 快速开始
10
+
11
+
12
+
13
+ **启动应用**:
14
+ ```bash
15
+ N8N_ENTERPRISE_MOCK=true pnpm run dev
16
+ ```
17
+
18
+ ## 📋 可用的企业功能
19
+
20
+ 启用模拟后,以下企业功能将可用,同时会自动隐藏"This n8n instance is not licensed for production purposes."横幅:
21
+
22
+ ### 🔐 认证和安全
23
+ - ✅ SAML单点登录 (`feat:saml`)
24
+ - ✅ LDAP/Active Directory集成 (`feat:ldap`)
25
+ - ✅ OpenID Connect/OAuth 2.0 (`feat:oidc`)
26
+ - ✅ 高级权限管理 (`feat:advancedPermissions`)
27
+ - ✅ API密钥作用域 (`feat:apiKeyScopes`)
28
+
29
+ ### 👥 协作和项目管理
30
+ - ✅ 工作流和凭证共享 (`feat:sharing`)
31
+ - ✅ 项目管理员角色 (`feat:projectRole:admin`)
32
+ - ✅ 项目编辑者角色 (`feat:projectRole:editor`)
33
+ - ✅ 项目查看者角色 (`feat:projectRole:viewer`)
34
+ - ✅ 文件夹组织 (`feat:folders`)
35
+
36
+ ### 🛠️ 开发运维
37
+ - ✅ Git源代码控制 (`feat:sourceControl`)
38
+ - ✅ 环境变量管理 (`feat:variables`)
39
+ - ✅ 外部密钥管理 (`feat:externalSecrets`)
40
+ - ✅ 工作流版本历史 (`feat:workflowHistory`)
41
+ - ✅ 编辑器调试功能 (`feat:debugInEditor`)
42
+
43
+ ### 🏗️ 基础设施和扩展
44
+ - ✅ 多主实例高可用 (`feat:multipleMainInstances`)
45
+ - ✅ S3二进制数据存储 (`feat:binaryDataS3`)
46
+ - ✅ 工作节点监控 (`feat:workerView`)
47
+ - ✅ 日志流式传输 (`feat:logStreaming`)
48
+ - ✅ 高级执行过滤器 (`feat:advancedExecutionFilters`)
49
+
50
+ ### 🤖 AI和分析
51
+ - ✅ AI助手 (`feat:aiAssistant`)
52
+ - ✅ AI问答功能 (`feat:askAi`)
53
+ - ✅ AI积分系统 (`feat:aiCredits`)
54
+ - ✅ 洞察摘要视图 (`feat:insights:viewSummary`)
55
+ - ✅ 洞察仪表板 (`feat:insights:viewDashboard`)
56
+ - ✅ 按小时数据分析 (`feat:insights:viewHourlyData`)
57
+
58
+ ### 📦 其他功能
59
+ - ✅ 自定义节点注册表 (`feat:communityNodes:customRegistry`)
60
+ - ✅ API禁用控制 (`feat:apiDisabled`)
61
+
62
+ ## 🔧 配置选项
63
+
64
+ ### 环境变量
65
+
66
+ 在 `.env` 文件中设置:
67
+
68
+ ```bash
69
+ # 启用企业版模拟
70
+ N8N_ENTERPRISE_MOCK=true
71
+
72
+ # 设置为开发环境
73
+ NODE_ENV=development
74
+
75
+ # 禁用许可证服务器验证
76
+ N8N_LICENSE_SERVER_URL=
77
+ N8N_LICENSE_AUTO_RENEW_ENABLED=false
78
+
79
+ # 启用各种功能(可选)
80
+ N8N_SAML_ENABLED=true
81
+ N8N_LDAP_ENABLED=true
82
+ N8N_LOG_STREAMING_ENABLED=true
83
+ ```
84
+
85
+ ### 命令行参数
86
+
87
+ ```bash
88
+ # 使用命令行参数启用
89
+ npm run dev -- --enterprise-mock
90
+ ```
91
+
92
+ ### 代码中检查
93
+
94
+ ```javascript
95
+ // 检查是否启用了企业版模拟
96
+ if (process.env.N8N_ENTERPRISE_MOCK === 'true') {
97
+ console.log('Enterprise features available');
98
+ }
99
+ ```
100
+
101
+ ## 🧪 测试企业功能
102
+
103
+ ### 1. 测试用户权限管理
104
+ 1. 访问 http://localhost:5678
105
+ 2. 注册/登录账户
106
+ 3. 进入 Settings > Users & Roles
107
+ 4. 尝试邀请用户并分配不同角色
108
+
109
+ ### 2. 测试文件夹功能
110
+ 1. 在工作流页面点击 "New Folder"
111
+ 2. 创建文件夹并组织工作流
112
+
113
+ ### 3. 测试变量管理
114
+ 1. 进入 Settings > Variables
115
+ 2. 创建和管理环境变量
116
+
117
+ ### 4. 测试源代码控制
118
+ 1. 进入 Settings > Source Control
119
+ 2. 配置Git仓库连接
120
+
121
+ ### 5. 测试外部密钥
122
+ 1. 进入 Settings > External Secrets
123
+ 2. 配置外部密钥提供商
124
+
125
+
126
+
127
+
128
+ ### 检查当前状态
129
+ 启动应用后查看控制台输出,如果看到以下信息说明模拟已启用:
130
+ ```
131
+ 🚀 N8N ENTERPRISE MOCK ENABLED
132
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
133
+ All enterprise features are unlocked for testing!
134
+ ```
135
+
136
+ ## 🎯 横幅控制
137
+
138
+ ### 非生产许可证横幅
139
+
140
+ 如果你在页面顶部看到 **"This n8n instance is not licensed for production purposes."** 横幅,这是由以下机制控制的:
141
+
142
+ 1. **后端控制**: `packages/cli/src/services/frontend.service.ts:333`
143
+ ```typescript
144
+ showNonProdBanner: this.license.isLicensed(LICENSE_FEATURES.SHOW_NON_PROD_BANNER)
145
+ ```
146
+
147
+ 2. **前端显示**: `packages/frontend/editor-ui/src/stores/settings.store.ts:193-195`
148
+ ```typescript
149
+ if (settings.value.enterprise?.showNonProdBanner) {
150
+ useUIStore().pushBannerToStack('NON_PRODUCTION_LICENSE');
151
+ }
152
+ ```
153
+
154
+ 3. **模拟处理**: 我们的企业版模拟会自动将 `SHOW_NON_PROD_BANNER` 设置为 `false`,从而隐藏这个横幅。
155
+
156
+ ### 手动隐藏横幅
157
+
158
+ 如果横幅仍然显示,请确保:
159
+ - 企业版模拟已正确启用 (`N8N_ENTERPRISE_MOCK=true`)
160
+ - 重新启动应用
161
+ - 清除浏览器缓存
162
+
163
+ ## 🛠️ 技术实现
164
+
165
+ ### 核心文件
166
+ - `packages/cli/src/license-mock-enterprise.ts` - 企业版模拟核心逻辑
167
+ - `packages/cli/src/init-enterprise-mock.ts` - 初始化模拟功能
168
+ - `packages/cli/src/commands/base-command.ts` - 修改了 `base-command.ts` 文件,添加了企业版模拟功能
169
+ - `.env.enterprise-mock` - 企业版环境配置模板
170
+
171
+ ### 工作原理
172
+ 1. **许可证拦截**: 覆盖 `License` 服务的 `isLicensed()` 和 `getValue()` 方法
173
+ 2. **功能启用**: 所有企业功能检查都返回 `true`
174
+ 3. **配额设置**: 所有配额限制都设置为无限制 (`-1`)
175
+ 4. **状态模拟**: 模拟企业版许可证状态和信息
176
+
177
+ ### 安全考虑
178
+ - 需要显式设置 `N8N_ENTERPRISE_MOCK=true`
179
+ - 启动时会显示明显的警告信息
180
+
181
+ ## 🐛 故障排除
182
+
183
+ ### 问题1: 模拟未启用
184
+ **解决方案**:
185
+ 1. 确认环境变量 `N8N_ENTERPRISE_MOCK=true`
186
+
187
+
188
+ ### 问题2: 某些功能仍然被锁定
189
+ **解决方案**:
190
+ 1. 重启应用
191
+ 2. 检查浏览器控制台是否有错误
192
+ 3. 确认前端许可证状态已更新
193
+
194
+
195
+ ## 📞 支持
196
+
197
+ 如果遇到问题,请检查:
198
+ 1. 控制台输出中的企业版模拟状态
199
+ 2. 环境变量设置
200
+ 3. 补丁是否正确应用
201
+
202
+ 这个模拟系统让你可以完整测试n8n的所有企业功能,而无需购买许可证。记住,这仅用于开发和测试目的!
LICENSE.md ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # License
2
+
3
+ Portions of this software are licensed as follows:
4
+
5
+ - Content of branches other than the main branch (i.e. "master") are not licensed.
6
+ - Source code files that contain ".ee." in their filename or ".ee" in their dirname are NOT licensed under
7
+ the Sustainable Use License.
8
+ To use source code files that contain ".ee." in their filename or ".ee" in their dirname you must hold a
9
+ valid n8n Enterprise License specifically allowing you access to such source code files and as defined
10
+ in "LICENSE_EE.md".
11
+ - All third party components incorporated into the n8n Software are licensed under the original license
12
+ provided by the owner of the applicable component.
13
+ - Content outside of the above mentioned files or restrictions is available under the "Sustainable Use
14
+ License" as defined below.
15
+
16
+ ## Sustainable Use License
17
+
18
+ Version 1.0
19
+
20
+ ### Acceptance
21
+
22
+ By using the software, you agree to all of the terms and conditions below.
23
+
24
+ ### Copyright License
25
+
26
+ The licensor grants you a non-exclusive, royalty-free, worldwide, non-sublicensable, non-transferable license
27
+ to use, copy, distribute, make available, and prepare derivative works of the software, in each case subject
28
+ to the limitations below.
29
+
30
+ ### Limitations
31
+
32
+ You may use or modify the software only for your own internal business purposes or for non-commercial or
33
+ personal use. You may distribute the software or provide it to others only if you do so free of charge for
34
+ non-commercial purposes. You may not alter, remove, or obscure any licensing, copyright, or other notices of
35
+ the licensor in the software. Any use of the licensor’s trademarks is subject to applicable law.
36
+
37
+ ### Patents
38
+
39
+ The licensor grants you a license, under any patent claims the licensor can license, or becomes able to
40
+ license, to make, have made, use, sell, offer for sale, import and have imported the software, in each case
41
+ subject to the limitations and conditions in this license. This license does not cover any patent claims that
42
+ you cause to be infringed by modifications or additions to the software. If you or your company make any
43
+ written claim that the software infringes or contributes to infringement of any patent, your patent license
44
+ for the software granted under these terms ends immediately. If your company makes such a claim, your patent
45
+ license ends immediately for work on behalf of your company.
46
+
47
+ ### Notices
48
+
49
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these
50
+ terms. If you modify the software, you must include in any modified copies of the software a prominent notice
51
+ stating that you have modified the software.
52
+
53
+ ### No Other Rights
54
+
55
+ These terms do not imply any licenses other than those expressly granted in these terms.
56
+
57
+ ### Termination
58
+
59
+ If you use the software in violation of these terms, such use is not licensed, and your license will
60
+ automatically terminate. If the licensor provides you with a notice of your violation, and you cease all
61
+ violation of this license no later than 30 days after you receive that notice, your license will be reinstated
62
+ retroactively. However, if you violate these terms after such reinstatement, any additional violation of these
63
+ terms will cause your license to terminate automatically and permanently.
64
+
65
+ ### No Liability
66
+
67
+ As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will
68
+ not be liable to you for any damages arising out of these terms or the use or nature of the software, under
69
+ any kind of legal claim.
70
+
71
+ ### Definitions
72
+
73
+ The “licensor” is the entity offering these terms.
74
+
75
+ The “software” is the software the licensor makes available under these terms, including any portion of it.
76
+
77
+ “You” refers to the individual or entity agreeing to these terms.
78
+
79
+ “Your company” is any legal entity, sole proprietorship, or other kind of organization that you work for, plus
80
+ all organizations that have control over, are under the control of, or are under common control with that
81
+ organization. Control means ownership of substantially all the assets of an entity, or the power to direct its
82
+ management and policies by vote, contract, or otherwise. Control can be direct or indirect.
83
+
84
+ “Your license” is the license granted to you for the software under these terms.
85
+
86
+ “Use” means anything you do with the software requiring your license.
87
+
88
+ “Trademark” means trademarks, service marks, and similar rights.
LICENSE_EE.md ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # The n8n Enterprise License (the “Enterprise License”)
2
+
3
+ Copyright (c) 2022-present n8n GmbH.
4
+
5
+ With regard to the n8n Software:
6
+
7
+ This software and associated documentation files (the "Software") may only be used in production, if
8
+ you (and any entity that you represent) hold a valid n8n Enterprise license corresponding to your
9
+ usage. Subject to the foregoing sentence, you are free to modify this Software and publish patches
10
+ to the Software. You agree that n8n and/or its licensors (as applicable) retain all right, title and
11
+ interest in and to all such modifications and/or patches, and all such modifications and/or patches
12
+ may only be used, copied, modified, displayed, distributed, or otherwise exploited with a valid n8n
13
+ Enterprise license for the corresponding usage. Notwithstanding the foregoing, you may copy and
14
+ modify the Software for development and testing purposes, without requiring a subscription. You
15
+ agree that n8n and/or its licensors (as applicable) retain all right, title and interest in and to
16
+ all such modifications. You are not granted any other rights beyond what is expressly stated herein.
17
+ Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense, and/or
18
+ sell the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
21
+ NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
23
+ OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
24
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
25
+
26
+ For all third party components incorporated into the n8n Software, those components are licensed
27
+ under the original license provided by the owner of the applicable component.
README-HUGGINGFACE.md ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # n8n 中文版 - Hugging Face 部署指南
2
+
3
+ 本指南将帮助您在 Hugging Face Spaces 上部署 n8n 中文版,使用 PostgreSQL 数据库。
4
+
5
+ ## 🚀 快速部署
6
+
7
+ ### 1. 创建 Hugging Face Space
8
+
9
+ 1. 访问 [Hugging Face Spaces](https://huggingface.co/spaces)
10
+ 2. 点击 "Create new Space"
11
+ 3. 选择 "Docker" 作为 SDK
12
+ 4. 设置 Space 名称(例如:`n8n-chs`)
13
+ 5. 选择合适的硬件配置(推荐 CPU basic 或更高)
14
+
15
+ ### 2. 准备 PostgreSQL 数据库
16
+
17
+ 您需要一个外部 PostgreSQL 数据库。推荐的免费选项:
18
+
19
+ - **Supabase**: https://supabase.com (推荐)
20
+ - **ElephantSQL**: https://www.elephantsql.com
21
+ - **Aiven**: https://aiven.io
22
+ - **Railway**: https://railway.app
23
+
24
+ ### 3. 配置环境变量
25
+
26
+ 在 Hugging Face Space 的 Settings 中添加以下环境变量:
27
+
28
+ #### 必需的数据库环境变量
29
+ ```bash
30
+ DB_TYPE=postgresdb
31
+ DB_POSTGRESDB_HOST=your-postgres-host.com
32
+ DB_POSTGRESDB_PORT=5432
33
+ DB_POSTGRESDB_DATABASE=your-database-name
34
+ DB_POSTGRESDB_USER=your-username
35
+ DB_POSTGRESDB_PASSWORD=your-password
36
+ ```
37
+
38
+ #### n8n 配置环境变量
39
+ ```bash
40
+ N8N_DEFAULT_LOCALE=zh-CN
41
+ N8N_ENTERPRISE_MOCK=true
42
+ N8N_PORT=7860
43
+ WEBHOOK_URL=https://your-space-name-n8n-chs.hf.space
44
+ ```
45
+
46
+ #### 可选的安全和功能配置
47
+ ```bash
48
+ N8N_ENCRYPTION_KEY=your-32-character-encryption-key
49
+ N8N_USER_MANAGEMENT_JWT_SECRET=your-jwt-secret
50
+ N8N_METRICS=false
51
+ N8N_DIAGNOSTICS_ENABLED=false
52
+ N8N_VERSION_NOTIFICATIONS_ENABLED=false
53
+ N8N_ONBOARDING_FLOW_DISABLED=true
54
+ N8N_HIRING_BANNER_ENABLED=false
55
+ ```
56
+
57
+ ### 4. 上传文件
58
+
59
+ 将以下文件上传到您的 Hugging Face Space:
60
+
61
+ 1. `Dockerfile` - 重命名 `Dockerfile.hf` 为 `Dockerfile`
62
+ 2. `start-hf.sh` - 启动脚本
63
+ 3. 整个 n8n 项目源代码
64
+
65
+ ### 5. 创建 Dockerfile
66
+
67
+ 在 Space 根目录创建 `Dockerfile`(复制 `Dockerfile.hf` 的内容):
68
+
69
+ ```dockerfile
70
+ # 使用提供的 Dockerfile.hf 内容
71
+ ```
72
+
73
+ ## 📋 环境变量详解
74
+
75
+ ### 数据库配置
76
+ | 变量名 | 必需 | 说明 | 示例 |
77
+ |--------|------|------|------|
78
+ | `DB_TYPE` | ✅ | 数据库类型 | `postgresdb` |
79
+ | `DB_POSTGRESDB_HOST` | ✅ | PostgreSQL 主机地址 | `db.example.com` |
80
+ | `DB_POSTGRESDB_PORT` | ❌ | PostgreSQL 端口 | `5432` |
81
+ | `DB_POSTGRESDB_DATABASE` | ✅ | 数据库名称 | `n8n` |
82
+ | `DB_POSTGRESDB_USER` | ✅ | 数据库用户名 | `n8n_user` |
83
+ | `DB_POSTGRESDB_PASSWORD` | ✅ | 数据库密码 | `your_secure_password` |
84
+
85
+ ### n8n 基础配置
86
+ | 变量名 | 必需 | 说明 | 默认值 |
87
+ |--------|------|------|--------|
88
+ | `N8N_PORT` | ❌ | 应用端口 | `7860` |
89
+ | `N8N_DEFAULT_LOCALE` | ❌ | 默认语言 | `zh-CN` |
90
+ | `N8N_ENTERPRISE_MOCK` | ❌ | 启用企业功能模拟 | `true` |
91
+ | `WEBHOOK_URL` | ❌ | Webhook 基础URL | 自动检测 |
92
+
93
+ ### 安全配置
94
+ | 变量名 | 必需 | 说明 |
95
+ |--------|------|------|
96
+ | `N8N_ENCRYPTION_KEY` | 🔒 | 数据加密密钥(32字符) |
97
+ | `N8N_USER_MANAGEMENT_JWT_SECRET` | 🔒 | JWT 密钥 |
98
+
99
+ ## 🔧 部署步骤
100
+
101
+ 1. **准备数据库**
102
+ - 创建 PostgreSQL 数据库实例
103
+ - 记录连接信息
104
+
105
+ 2. **配置 Space**
106
+ - 创建 Hugging Face Space
107
+ - 设置环境变量
108
+ - 上传代码文件
109
+
110
+ 3. **部署应用**
111
+ - Space 会自动构建和部署
112
+ - 查看构建日志确认无错误
113
+
114
+ 4. **首次访问**
115
+ - 访问您的 Space URL
116
+ - 创建管理员账户
117
+ - 开始使用 n8n
118
+
119
+ ## 🛠️ 故障排除
120
+
121
+ ### 常见问题
122
+
123
+ **1. 数据库连接失败**
124
+ - 检查数据库环境变量是否正确
125
+ - 确认数据库服务器允许外部连接
126
+ - 验证用户名和密码
127
+
128
+ **2. 应用无法启动**
129
+ - 查看 Space 的构建日志
130
+ - 检查 Dockerfile 语法
131
+ - 确认所有必需的环境变量已设置
132
+
133
+ **3. Webhook 不工作**
134
+ - 确认 `WEBHOOK_URL` 设置正确
135
+ - 检查 Space 的公开访问权限
136
+
137
+ ### 日志查看
138
+
139
+ 在 Hugging Face Space 的 "Logs" 标签页可以查看:
140
+ - 构建日志
141
+ - 运行时日志
142
+ - 错误信息
143
+
144
+ ## 📚 更多资源
145
+
146
+ - [n8n 官方文档](https://docs.n8n.io)
147
+ - [Hugging Face Spaces 文档](https://huggingface.co/docs/hub/spaces)
148
+ - [PostgreSQL 文档](https://www.postgresql.org/docs/)
149
+
150
+ ## 🤝 支持
151
+
152
+ 如果遇到问题,可以:
153
+ 1. 查看 Space 的构建和运行日志
154
+ 2. 检查环境变量配置
155
+ 3. 参考 n8n 官方文档
156
+ 4. 在项目 GitHub 仓库提交 Issue
157
+
158
+ ---
159
+
160
+ **注意**: 请确保妥善保管数据库凭据和加密密钥,不要在公开场所泄露这些敏感信息。
SECURITY.md ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ ## Reporting a Vulnerability
2
+
3
+ Please report (suspected) security vulnerabilities to **[security@n8n.io](mailto:security@n8n.io)**. You will receive a response from
4
+ us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
assets/n8n-logo.png ADDED

Git LFS Details

  • SHA256: 5fd41f2b2e0026bb20e823259e6ab41ee566b1af5285ed41d4b2a82fdac3341a
  • Pointer size: 129 Bytes
  • Size of remote file: 7.9 kB
assets/n8n-screenshot-readme.png ADDED

Git LFS Details

  • SHA256: 0e7b351afc2f520b686effff33be5be5be7482ef1fbeb6936c11b90d5b741083
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
assets/n8n-screenshot.png ADDED

Git LFS Details

  • SHA256: de2f9b3f8f6567388d9ea3b054e7aeb20d7b98dabf1e164f5dfb532ffcbe3620
  • Pointer size: 131 Bytes
  • Size of remote file: 229 kB
biome.jsonc ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
3
+ "vcs": {
4
+ "clientKind": "git",
5
+ "enabled": true,
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignore": [
10
+ "**/.turbo",
11
+ "**/coverage",
12
+ "**/dist",
13
+ "**/package.json",
14
+ "**/pnpm-lock.yaml",
15
+ "**/CHANGELOG.md",
16
+ "**/cl100k_base.json",
17
+ "**/o200k_base.json"
18
+ ]
19
+ },
20
+ "formatter": {
21
+ "enabled": true,
22
+ "formatWithErrors": false,
23
+ "indentStyle": "tab",
24
+ "indentWidth": 2,
25
+ "lineEnding": "lf",
26
+ "lineWidth": 100,
27
+ "attributePosition": "auto",
28
+ "ignore": [
29
+ // Handled by prettier
30
+ "**/*.vue"
31
+ ]
32
+ },
33
+ "organizeImports": { "enabled": false },
34
+ "linter": {
35
+ "enabled": false
36
+ },
37
+ "javascript": {
38
+ "parser": {
39
+ "unsafeParameterDecoratorsEnabled": true
40
+ },
41
+ "formatter": {
42
+ "jsxQuoteStyle": "double",
43
+ "quoteProperties": "asNeeded",
44
+ "trailingCommas": "all",
45
+ "semicolons": "always",
46
+ "arrowParentheses": "always",
47
+ "bracketSpacing": true,
48
+ "bracketSameLine": false,
49
+ "quoteStyle": "single",
50
+ "attributePosition": "auto"
51
+ }
52
+ }
53
+ }
codecov.yml ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ codecov:
2
+ max_report_age: off
3
+ require_ci_to_pass: true
4
+
5
+ coverage:
6
+ status:
7
+ patch: false
8
+ project:
9
+ default:
10
+ threshold: 0.5
11
+
12
+ github_checks:
13
+ annotations: false
14
+
15
+ flags:
16
+ tests:
17
+ paths:
18
+ - '**'
19
+ carryforward: true
20
+
21
+ component_management:
22
+ default_rules:
23
+ statuses:
24
+ - type: project
25
+ target: auto
26
+ branches:
27
+ - '!master'
28
+ individual_components:
29
+ - component_id: backend_packages
30
+ name: Backend
31
+ paths:
32
+ - packages/@n8n/api-types/**
33
+ - packages/@n8n/config/**
34
+ - packages/@n8n/client-oauth2/**
35
+ - packages/@n8n/decorators/**
36
+ - packages/@n8n/constants/**
37
+ - packages/@n8n/backend-common/**
38
+ - packages/@n8n/backend-test-utils/**
39
+ - packages/@n8n/db/**
40
+ - packages/@n8n/di/**
41
+ - packages/@n8n/imap/**
42
+ - packages/@n8n/permissions/**
43
+ - packages/@n8n/task-runner/**
44
+ - packages/workflow/**
45
+ - packages/core/**
46
+ - packages/cli/**
47
+ - component_id: frontend_packages
48
+ name: Frontend
49
+ paths:
50
+ - packages/@n8n/codemirror-lang/**
51
+ - packages/frontend/**
52
+ - component_id: nodes_packages
53
+ name: Nodes
54
+ paths:
55
+ - packages/node-dev/**
56
+ - packages/nodes-base/**
57
+ - packages/@n8n/json-schema-to-zod/**
58
+ - packages/@n8n/nodes-langchain/**
59
+ statuses:
60
+ - type: project
61
+ target: auto
62
+ threshold: 0% # Enforce: Coverage must not decrease
63
+
64
+ ignore:
65
+ - (?s:.*/[^\/]*\.spec\.ts.*)\Z
66
+ - (?s:.*/[^\/]*\.test\.ts.*)\Z
67
+ - (?s:.*/[^\/]*e2e[^\/]*\.ts.*)\Z
correct_compare.js ADDED
@@ -0,0 +1,110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const fs = require('fs');
2
+
3
+ function main() {
4
+ console.log('=== 正确的JSON文件比较 (针对扁平化键) ===');
5
+
6
+ try {
7
+ // 读取两个文件
8
+ console.log('读取文件...');
9
+ const enContent = fs.readFileSync('packages/frontend/@n8n/i18n/src/locales/en.json', 'utf8');
10
+ const enJson = JSON.parse(enContent);
11
+
12
+ const zhContent = fs.readFileSync('packages/frontend/@n8n/i18n/src/locales/zh-CN.json', 'utf8');
13
+ const zhJson = JSON.parse(zhContent);
14
+
15
+ // 直接获取键,因为en.json已经是扁平化的
16
+ const enKeys = Object.keys(enJson);
17
+ const zhKeys = Object.keys(zhJson);
18
+
19
+ console.log(`en.json 键数量: ${enKeys.length}`);
20
+ console.log(`zh-CN.json 键数量: ${zhKeys.length}`);
21
+
22
+ // 找出缺失的键
23
+ const missingKeys = enKeys.filter((key) => !(key in zhJson));
24
+
25
+ console.log(`zh-CN.json 中缺失的键数量: ${missingKeys.length}`);
26
+
27
+ if (missingKeys.length === 0) {
28
+ console.log('✅ 完美!所有键都已同步。');
29
+
30
+ // 清理旧的差异文件
31
+ if (fs.existsSync('missing_translations.json')) {
32
+ fs.unlinkSync('missing_translations.json');
33
+ console.log('移除了旧的差异文件');
34
+ }
35
+ return;
36
+ }
37
+
38
+ // 显示前10个缺失的键
39
+ console.log('\n前10个缺失的键:');
40
+ missingKeys.slice(0, 10).forEach((key) => {
41
+ console.log(` - ${key}: "${enJson[key]}"`);
42
+ });
43
+
44
+ // 创建差异对象
45
+ console.log('\n创建差异文件...');
46
+ const diffObject = {};
47
+ missingKeys.forEach((key) => {
48
+ diffObject[key] = enJson[key];
49
+ });
50
+
51
+ // 写入差异文件
52
+ fs.writeFileSync('missing_translations.json', JSON.stringify(diffObject, null, 2), 'utf8');
53
+ console.log(`✔️ 已将 ${missingKeys.length} 个缺失的键值对写入 missing_translations.json`);
54
+
55
+ // 更新 zh-CN.json
56
+ console.log('\n更新 zh-CN.json...');
57
+
58
+ // 创建更新后的对象
59
+ const updatedZhJson = { ...zhJson };
60
+
61
+ // 直接添加缺失的键
62
+ missingKeys.forEach((key) => {
63
+ updatedZhJson[key] = enJson[key];
64
+ });
65
+
66
+ console.log(`成功添加了 ${missingKeys.length} 个键`);
67
+
68
+ // 写入更新后的文件
69
+ fs.writeFileSync(
70
+ 'packages/frontend/@n8n/i18n/src/locales/zh-CN.json',
71
+ JSON.stringify(updatedZhJson, null, 2),
72
+ 'utf8',
73
+ );
74
+ console.log('✔️ zh-CN.json 已更新');
75
+
76
+ // 最终验证
77
+ console.log('\n=== 最终验证 ===');
78
+ const verifyContent = fs.readFileSync(
79
+ 'packages/frontend/@n8n/i18n/src/locales/zh-CN.json',
80
+ 'utf8',
81
+ );
82
+ const verifyJson = JSON.parse(verifyContent);
83
+
84
+ const stillMissing = enKeys.filter((key) => !(key in verifyJson));
85
+
86
+ if (stillMissing.length === 0) {
87
+ console.log('✅ 验证成功!所有键现已同步。');
88
+ console.log(`最终键数量:${Object.keys(verifyJson).length}`);
89
+ } else {
90
+ console.log(`❌ 验证失败!仍有 ${stillMissing.length} 个键缺失:`);
91
+ stillMissing.slice(0, 5).forEach((key) => {
92
+ console.log(` - ${key}`);
93
+ });
94
+ }
95
+
96
+ // 检查是否有多余的键
97
+ const extraKeys = zhKeys.filter((key) => !(key in enJson));
98
+ if (extraKeys.length > 0) {
99
+ console.log(`\nℹ️ zh-CN.json 中有 ${extraKeys.length} 个额外的键(不存在于 en.json 中):`);
100
+ extraKeys.slice(0, 5).forEach((key) => {
101
+ console.log(` - ${key}`);
102
+ });
103
+ }
104
+ } catch (error) {
105
+ console.error('❌ 错误:', error.message);
106
+ console.error(error.stack);
107
+ }
108
+ }
109
+
110
+ main();
cypress/.eslintrc.js ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const sharedOptions = require('@n8n/eslint-config/shared');
2
+
3
+ /**
4
+ * @type {import('@types/eslint').ESLint.ConfigData}
5
+ */
6
+ module.exports = {
7
+ extends: ['@n8n/eslint-config/base', 'plugin:cypress/recommended'],
8
+
9
+ ...sharedOptions(__dirname),
10
+
11
+ plugins: ['cypress'],
12
+
13
+ env: {
14
+ 'cypress/globals': true,
15
+ },
16
+
17
+ rules: {
18
+ // TODO: remove these rules
19
+ '@typescript-eslint/no-explicit-any': 'off',
20
+ '@typescript-eslint/no-unsafe-argument': 'off',
21
+ '@typescript-eslint/no-unsafe-assignment': 'off',
22
+ '@typescript-eslint/no-unsafe-call': 'off',
23
+ '@typescript-eslint/no-unsafe-member-access': 'off',
24
+ '@typescript-eslint/no-unsafe-return': 'off',
25
+ '@typescript-eslint/no-unused-expressions': 'off',
26
+ '@typescript-eslint/no-use-before-define': 'off',
27
+ '@typescript-eslint/promise-function-async': 'off',
28
+ 'n8n-local-rules/no-uncaught-json-parse': 'off',
29
+ 'cypress/no-assigning-return-values': 'warn',
30
+ 'cypress/no-unnecessary-waiting': 'warn',
31
+ 'cypress/unsafe-to-chain-command': 'warn',
32
+ 'import/no-extraneous-dependencies': [
33
+ 'error',
34
+ {
35
+ devDependencies: ['**/cypress/**'],
36
+ optionalDependencies: false,
37
+ },
38
+ ],
39
+ },
40
+ };
cypress/.gitignore ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ videos/
2
+ screenshots/
3
+ downloads/
cypress/README.md ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Debugging Flaky End-to-End Tests - Usage
2
+
3
+ To debug flaky end-to-end (E2E) tests, use the following command:
4
+
5
+ ```bash
6
+ pnpm run debug:flaky:e2e -- <grep_filter> <burn_count>
7
+ ```
8
+
9
+ **Parameters:**
10
+
11
+ * `<grep_filter>`: (Optional) A string to filter tests by their `it()` or `describe()` block titles, or by tags if using the `@cypress/grep` plugin. If omitted, all tests will be run.
12
+ * `<burn_count>`: (Optional) The number of times to run the filtered tests. Defaults to 5 if not provided.
13
+
14
+ **Examples:**
15
+
16
+ 1. **Run all tests tagged with `CAT-726` ten times:**
17
+
18
+ ```bash
19
+ pnpm run debug:flaky:e2e CAT-726 10
20
+ ```
21
+
22
+ 2. **Run all tests containing "login" five times (default burn count):**
23
+
24
+ ```bash
25
+ pnpm run debug:flaky:e2e login
26
+ ```
27
+
28
+ 3. **Run all tests five times (default grep and burn count):**
29
+
30
+ ```bash
31
+ pnpm run debug:flaky:e2e
32
+ ```
cypress/augmentation.d.ts ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ declare module 'cypress-otp' {
2
+ // eslint-disable-next-line import/no-default-export
3
+ export default function generateOTPToken(secret: string): string;
4
+ }
cypress/biome.jsonc ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "$schema": "../node_modules/@biomejs/biome/configuration_schema.json",
3
+ "extends": ["../biome.jsonc"],
4
+ "formatter": {
5
+ "ignore": ["fixtures/**"]
6
+ }
7
+ }
cypress/composables/becomeTemplateCreatorCta.ts ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ //#region Getters
2
+
3
+ export const getBecomeTemplateCreatorCta = () => cy.getByTestId('become-template-creator-cta');
4
+
5
+ export const getCloseBecomeTemplateCreatorCtaButton = () =>
6
+ cy.getByTestId('close-become-template-creator-cta');
7
+
8
+ //#endregion
9
+
10
+ //#region Actions
11
+
12
+ export const interceptCtaRequestWithResponse = (becomeCreator: boolean) => {
13
+ return cy.intercept('GET', '/rest/cta/become-creator', {
14
+ body: becomeCreator,
15
+ });
16
+ };
17
+
18
+ //#endregion
cypress/composables/create.ts ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const universalAddButton = () => cy.getByTestId('universal-add');
2
+
3
+ export const createResource = (
4
+ resourceType: 'project' | 'workflow' | 'credential',
5
+ projectName: string,
6
+ ) => {
7
+ universalAddButton().click();
8
+ cy.getByTestId('navigation-submenu')
9
+ .contains(new RegExp(resourceType, 'i'))
10
+ .should('be.visible')
11
+ .click();
12
+
13
+ if (resourceType !== 'project') {
14
+ cy.getByTestId('navigation-submenu-item')
15
+ .contains(new RegExp(projectName))
16
+ .should('be.visible')
17
+ .click();
18
+ }
19
+ };
cypress/composables/credentialsComposables.ts ADDED
@@ -0,0 +1,114 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function getCredentialsPageUrl() {
2
+ return '/home/credentials';
3
+ }
4
+
5
+ export const verifyCredentialsListPageIsLoaded = () => {
6
+ cy.get('[data-test-id="resources-list-wrapper"], [data-test-id="empty-resources-list"]').should(
7
+ 'be.visible',
8
+ );
9
+ };
10
+
11
+ export const loadCredentialsPage = (credentialsPageUrl: string) => {
12
+ cy.visit(credentialsPageUrl);
13
+ verifyCredentialsListPageIsLoaded();
14
+ };
15
+
16
+ /**
17
+ * Getters - Page
18
+ */
19
+
20
+ export function getEmptyListCreateCredentialButton() {
21
+ return cy.getByTestId('empty-resources-list').find('button');
22
+ }
23
+
24
+ export function getCredentialCards() {
25
+ return cy.getByTestId('resources-list-item');
26
+ }
27
+
28
+ /**
29
+ * Getters - Modal
30
+ */
31
+
32
+ export function getNewCredentialModal() {
33
+ return cy.getByTestId('selectCredential-modal', { timeout: 5000 });
34
+ }
35
+
36
+ export function getEditCredentialModal() {
37
+ return cy.getByTestId('editCredential-modal', { timeout: 5000 });
38
+ }
39
+
40
+ export function getNewCredentialTypeSelect() {
41
+ return cy.getByTestId('new-credential-type-select');
42
+ }
43
+
44
+ export function getNewCredentialTypeOption(credentialType: string) {
45
+ return cy.getByTestId('new-credential-type-select-option').contains(credentialType);
46
+ }
47
+
48
+ export function getNewCredentialTypeButton() {
49
+ return cy.getByTestId('new-credential-type-button');
50
+ }
51
+
52
+ export function getCredentialConnectionParameterInputs() {
53
+ return cy.getByTestId('credential-connection-parameter');
54
+ }
55
+
56
+ export function getConnectionParameter(fieldName: string) {
57
+ return getCredentialConnectionParameterInputs().find(
58
+ `:contains('${fieldName}') .n8n-input input`,
59
+ );
60
+ }
61
+
62
+ export function getCredentialSaveButton() {
63
+ return cy.getByTestId('credential-save-button', { timeout: 5000 });
64
+ }
65
+
66
+ /**
67
+ * Actions - Modal
68
+ */
69
+
70
+ export function setCredentialName(name: string) {
71
+ cy.getByTestId('credential-name').find('span[data-test-id=inline-edit-preview]').click();
72
+ cy.getByTestId('credential-name').type(name);
73
+ }
74
+ export function saveCredential() {
75
+ getCredentialSaveButton()
76
+ .click({ force: true })
77
+ .within(() => {
78
+ cy.get('button').should('not.exist');
79
+ });
80
+ getCredentialSaveButton().should('have.text', 'Saved');
81
+ }
82
+ export function saveCredentialWithWait() {
83
+ cy.intercept('POST', '/rest/credentials').as('saveCredential');
84
+ saveCredential();
85
+ cy.wait('@saveCredential');
86
+ getCredentialSaveButton().should('contain.text', 'Saved');
87
+ }
88
+
89
+ export function closeNewCredentialModal() {
90
+ getNewCredentialModal().find('.el-dialog__close').first().click();
91
+ }
92
+
93
+ export function createNewCredential(
94
+ type: string,
95
+ name: string,
96
+ parameter: string,
97
+ parameterValue: string,
98
+ closeModal = true,
99
+ ) {
100
+ getEmptyListCreateCredentialButton().click();
101
+
102
+ getNewCredentialModal().should('be.visible');
103
+ getNewCredentialTypeSelect().should('be.visible');
104
+ getNewCredentialTypeOption(type).click();
105
+
106
+ getNewCredentialTypeButton().click();
107
+ getConnectionParameter(parameter).type(parameterValue);
108
+
109
+ setCredentialName(name);
110
+ saveCredential();
111
+ if (closeModal) {
112
+ getEditCredentialModal().find('.el-dialog__close').first().click();
113
+ }
114
+ }
cypress/composables/executions.ts ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Getters
3
+ */
4
+
5
+ export const getExecutionsSidebar = () => cy.getByTestId('executions-sidebar');
6
+
7
+ export const getWorkflowExecutionPreviewIframe = () => cy.getByTestId('workflow-preview-iframe');
8
+
9
+ export const getExecutionPreviewBody = () =>
10
+ getWorkflowExecutionPreviewIframe()
11
+ .its('0.contentDocument.body')
12
+ .should((body) => {
13
+ expect(body.querySelector('[data-test-id="canvas-wrapper"]')).to.exist;
14
+ })
15
+ .then((el) => cy.wrap(el));
16
+
17
+ export const getExecutionPreviewBodyNodes = () =>
18
+ getExecutionPreviewBody().findChildByTestId('canvas-node');
19
+
20
+ export const getExecutionPreviewBodyNodesByName = (name: string) =>
21
+ getExecutionPreviewBody().findChildByTestId('canvas-node').filter(`[data-name="${name}"]`).eq(0);
22
+
23
+ export function getExecutionPreviewOutputPanelRelatedExecutionLink() {
24
+ return getExecutionPreviewBody().findChildByTestId('related-execution-link');
25
+ }
26
+
27
+ export function getLogsOverviewStatus() {
28
+ return getExecutionPreviewBody().findChildByTestId('logs-overview-status');
29
+ }
30
+
31
+ export function getLogEntries() {
32
+ return getExecutionPreviewBody().findChildByTestId('logs-overview-body').find('[role=treeitem]');
33
+ }
34
+
35
+ export function getManualChatMessages() {
36
+ return getExecutionPreviewBody().find('.chat-messages-list .chat-message');
37
+ }
38
+
39
+ /**
40
+ * Actions
41
+ */
42
+
43
+ export const openExecutionPreviewNode = (name: string) =>
44
+ getExecutionPreviewBodyNodesByName(name).dblclick();
45
+
46
+ export const toggleAutoRefresh = () => cy.getByTestId('auto-refresh-checkbox').click();
cypress/composables/featureFlags.ts ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export const overrideFeatureFlag = (name: string, value: boolean | string) => {
2
+ cy.window().then((win) => {
3
+ // If feature flags hasn't been initialized yet, we store the override
4
+ // in local storage and it gets loaded when the feature flags are
5
+ // initialized.
6
+ win.localStorage.setItem('N8N_EXPERIMENT_OVERRIDES', JSON.stringify({ [name]: value }));
7
+
8
+ if (win.featureFlags) {
9
+ win.featureFlags.override(name, value);
10
+ }
11
+ });
12
+ };
cypress/composables/folders.ts ADDED
@@ -0,0 +1,553 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { errorToast, successToast } from '../pages/notifications';
2
+
3
+ /**
4
+ * Getters
5
+ */
6
+ export function getPersonalProjectMenuItem() {
7
+ return cy.getByTestId('project-personal-menu-item');
8
+ }
9
+
10
+ export function getOverviewMenuItem() {
11
+ return cy.getByTestId('menu-item').contains('Overview');
12
+ }
13
+
14
+ export function getAddResourceDropdown() {
15
+ return cy.getByTestId('add-resource');
16
+ }
17
+
18
+ export function getFolderCards() {
19
+ return cy.getByTestId('folder-card');
20
+ }
21
+
22
+ export function getFolderCard(name: string) {
23
+ return cy.getByTestId('folder-card-name').contains(name).closest('[data-test-id="folder-card"]');
24
+ }
25
+
26
+ export function getWorkflowCards() {
27
+ return cy.getByTestId('resources-list-item-workflow');
28
+ }
29
+
30
+ export function getWorkflowCard(name: string) {
31
+ return cy
32
+ .getByTestId('workflow-card-name')
33
+ .contains(name)
34
+ .closest('[data-test-id="resources-list-item-workflow"]');
35
+ }
36
+
37
+ export function getWorkflowCardActions(name: string) {
38
+ return getWorkflowCard(name).find('[data-test-id="workflow-card-actions"]');
39
+ }
40
+
41
+ export function getWorkflowCardActionItem(workflowName: string, actionName: string) {
42
+ return getWorkflowCardActions(workflowName)
43
+ .find('span[aria-controls]')
44
+ .invoke('attr', 'aria-controls')
45
+ .then((popperId) => {
46
+ return cy.get(`#${popperId}`).find(`[data-test-id="action-${actionName}"]`);
47
+ });
48
+ }
49
+
50
+ export function getDuplicateWorkflowModal() {
51
+ return cy.getByTestId('duplicate-modal');
52
+ }
53
+
54
+ export function getWorkflowMenu() {
55
+ return cy.getByTestId('workflow-menu');
56
+ }
57
+
58
+ export function getAddFolderButton() {
59
+ return cy.getByTestId('add-folder-button');
60
+ }
61
+
62
+ export function getListBreadcrumbs() {
63
+ return cy.getByTestId('main-breadcrumbs');
64
+ }
65
+
66
+ export function getHomeProjectBreadcrumb() {
67
+ return getListBreadcrumbs().findChildByTestId('home-project');
68
+ }
69
+
70
+ export function getListBreadcrumbItem(name: string) {
71
+ return getListBreadcrumbs().findChildByTestId('breadcrumbs-item').contains(name);
72
+ }
73
+
74
+ export function getVisibleListBreadcrumbs() {
75
+ return getListBreadcrumbs().findChildByTestId('breadcrumbs-item');
76
+ }
77
+
78
+ export function getCurrentBreadcrumb() {
79
+ return getListBreadcrumbs().findChildByTestId('breadcrumbs-item-current').find('input');
80
+ }
81
+
82
+ export function getCurrentBreadcrumbText() {
83
+ return getCurrentBreadcrumb().invoke('val');
84
+ }
85
+
86
+ export function getMainBreadcrumbsEllipsis() {
87
+ return getListBreadcrumbs().findChildByTestId('hidden-items-menu');
88
+ }
89
+
90
+ export function getMainBreadcrumbsEllipsisMenuItems() {
91
+ return cy
92
+ .getByTestId('hidden-items-menu')
93
+ .find('span[aria-controls]')
94
+ .invoke('attr', 'aria-controls')
95
+ .then((popperId) => {
96
+ return cy.get(`#${popperId}`).find('li');
97
+ });
98
+ }
99
+
100
+ export function getFolderCardBreadCrumbs(folderName: string) {
101
+ return getFolderCard(folderName).find('[data-test-id="folder-card-breadcrumbs"]');
102
+ }
103
+
104
+ export function getFolderCardBreadCrumbsEllipsis(folderName: string) {
105
+ return getFolderCardBreadCrumbs(folderName).find('[data-test-id="ellipsis"]');
106
+ }
107
+
108
+ export function getFolderCardHomeProjectBreadcrumb(folderName: string) {
109
+ return getFolderCardBreadCrumbs(folderName).find('[data-test-id="folder-card-home-project"]');
110
+ }
111
+
112
+ export function getFolderCardCurrentBreadcrumb(folderName: string) {
113
+ return getFolderCardBreadCrumbs(folderName).find('[data-test-id="breadcrumbs-item-current"]');
114
+ }
115
+
116
+ export function getOpenHiddenItemsTooltip() {
117
+ return cy.getByTestId('hidden-items-tooltip').filter(':visible');
118
+ }
119
+
120
+ export function getListActionsToggle() {
121
+ return cy.getByTestId('folder-breadcrumbs-actions');
122
+ }
123
+
124
+ export function getCanvasBreadcrumbs() {
125
+ cy.getByTestId('canvas-breadcrumbs').should('exist');
126
+ return cy.getByTestId('canvas-breadcrumbs').findChildByTestId('folder-breadcrumbs');
127
+ }
128
+
129
+ export function getListActionItem(name: string) {
130
+ return cy
131
+ .getByTestId('folder-breadcrumbs-actions')
132
+ .find('span[aria-controls]')
133
+ .invoke('attr', 'aria-controls')
134
+ .then((popperId) => {
135
+ return cy.get(`#${popperId}`).find(`[data-test-id="action-${name}"]`);
136
+ });
137
+ }
138
+
139
+ export function getInlineEditInput() {
140
+ return cy.getByTestId('inline-edit-input');
141
+ }
142
+
143
+ export function getFolderCardActionToggle(folderName: string) {
144
+ return getFolderCard(folderName).find('[data-test-id="folder-card-actions"]');
145
+ }
146
+
147
+ export function getFolderCardActionItem(folderName: string, actionName: string) {
148
+ return getFolderCard(folderName)
149
+ .findChildByTestId('folder-card-actions')
150
+ .filter(':visible')
151
+ .find('span[aria-controls]')
152
+ .invoke('attr', 'aria-controls')
153
+ .then((popperId) => {
154
+ return cy.get(`#${popperId}`).find(`[data-test-id="action-${actionName}"]`);
155
+ });
156
+ }
157
+
158
+ export function getFolderDeleteModal() {
159
+ return cy.getByTestId('deleteFolder-modal');
160
+ }
161
+
162
+ export function getMoveFolderModal() {
163
+ return cy.getByTestId('moveFolder-modal');
164
+ }
165
+
166
+ export function getDeleteRadioButton() {
167
+ return cy.getByTestId('delete-content-radio');
168
+ }
169
+
170
+ export function getTransferContentRadioButton() {
171
+ return cy.getByTestId('transfer-content-radio');
172
+ }
173
+
174
+ export function getConfirmDeleteInput() {
175
+ return getFolderDeleteModal().findChildByTestId('delete-data-input').find('input');
176
+ }
177
+
178
+ export function getDeleteFolderModalConfirmButton() {
179
+ return getFolderDeleteModal().findChildByTestId('confirm-delete-folder-button');
180
+ }
181
+
182
+ export function getProjectEmptyState() {
183
+ return cy.getByTestId('list-empty-state');
184
+ }
185
+
186
+ export function getFolderEmptyState() {
187
+ return cy.getByTestId('empty-folder-container');
188
+ }
189
+
190
+ export function getProjectMenuItem(name: string) {
191
+ if (name.toLowerCase() === 'personal') {
192
+ return getPersonalProjectMenuItem();
193
+ }
194
+ return cy.getByTestId('project-menu-item').contains(name);
195
+ }
196
+
197
+ export function getMoveToFolderDropdown() {
198
+ return cy.getByTestId('move-to-folder-dropdown');
199
+ }
200
+
201
+ export function getMoveToFolderOption(name: string) {
202
+ return cy.getByTestId('move-to-folder-option').contains(name);
203
+ }
204
+
205
+ export function getMoveToFolderInput() {
206
+ return getMoveToFolderDropdown().find('input');
207
+ }
208
+
209
+ export function getProjectSharingInput() {
210
+ return cy.getByTestId('project-sharing-select');
211
+ }
212
+
213
+ export function getProjectSharingOption(name: string) {
214
+ return cy.getByTestId('project-sharing-info').contains(name);
215
+ }
216
+
217
+ export function getEmptyFolderDropdownMessage(text: string) {
218
+ return cy.get('.el-select-dropdown__empty').contains(text);
219
+ }
220
+
221
+ export function getMoveFolderConfirmButton() {
222
+ return cy.getByTestId('confirm-move-folder-button');
223
+ }
224
+
225
+ export function getMoveWorkflowModal() {
226
+ return cy.getByTestId('moveFolder-modal');
227
+ }
228
+
229
+ export function getWorkflowCardBreadcrumbs(workflowName: string) {
230
+ return getWorkflowCard(workflowName).find('[data-test-id="workflow-card-breadcrumbs"]');
231
+ }
232
+
233
+ export function getWorkflowCardBreadcrumbsEllipsis(workflowName: string) {
234
+ return getWorkflowCardBreadcrumbs(workflowName).find('[data-test-id="ellipsis"]');
235
+ }
236
+
237
+ export function getNewFolderNameInput() {
238
+ return cy.get('.add-folder-modal').filter(':visible').find('input.el-input__inner');
239
+ }
240
+
241
+ export function getNewFolderModalErrorMessage() {
242
+ return cy.get('.el-message-box__errormsg').filter(':visible');
243
+ }
244
+
245
+ export function getProjectTab(tabId: string) {
246
+ return cy.getByTestId('project-tabs').find(`#${tabId}`);
247
+ }
248
+
249
+ /**
250
+ * Actions
251
+ */
252
+ export function goToPersonalProject() {
253
+ getPersonalProjectMenuItem().click();
254
+ }
255
+
256
+ export function createFolderInsideFolder(childName: string, parentName: string) {
257
+ getFolderCard(parentName).click();
258
+ createFolderFromListHeaderButton(childName);
259
+ }
260
+
261
+ export function createFolderFromListHeaderButton(folderName: string) {
262
+ getAddFolderButton().click();
263
+ createNewFolder(folderName);
264
+ }
265
+
266
+ export function createWorkflowFromEmptyState(workflowName?: string) {
267
+ getFolderEmptyState().find('button').contains('Create Workflow').click();
268
+ if (workflowName) {
269
+ cy.getByTestId('workflow-name-input').type(`{selectAll}{backspace}${workflowName}`, {
270
+ delay: 50,
271
+ });
272
+ }
273
+ cy.getByTestId('workflow-save-button').click();
274
+ successToast().should('exist');
275
+ }
276
+
277
+ export function createWorkflowFromProjectHeader(folderName?: string, workflowName?: string) {
278
+ cy.getByTestId('add-resource-workflow').click();
279
+ if (workflowName) {
280
+ cy.getByTestId('workflow-name-input').type(`{selectAll}{backspace}${workflowName}`, {
281
+ delay: 50,
282
+ });
283
+ }
284
+ cy.getByTestId('workflow-save-button').click();
285
+ if (folderName) {
286
+ successToast().should(
287
+ 'contain.text',
288
+ `Workflow successfully created in "Personal", within "${folderName}"`,
289
+ );
290
+ }
291
+ }
292
+
293
+ export function createWorkflowFromListDropdown(workflowName?: string) {
294
+ getListActionsToggle().click();
295
+ getListActionItem('create_workflow').click();
296
+ if (workflowName) {
297
+ cy.getByTestId('workflow-name-input').type(`{selectAll}{backspace}${workflowName}`, {
298
+ delay: 50,
299
+ });
300
+ }
301
+ cy.getByTestId('workflow-save-button').click();
302
+ successToast().should('exist');
303
+ }
304
+
305
+ export function createFolderFromProjectHeader(folderName: string) {
306
+ getAddResourceDropdown().click();
307
+ cy.getByTestId('action-folder').click();
308
+ createNewFolder(folderName);
309
+ }
310
+
311
+ export function createFolderFromListDropdown(folderName: string) {
312
+ getListActionsToggle().click();
313
+ getListActionItem('create').click();
314
+ createNewFolder(folderName);
315
+ }
316
+
317
+ export function createFolderFromCardActions(parentName: string, folderName: string) {
318
+ getFolderCardActionToggle(parentName).click();
319
+ getFolderCardActionItem(parentName, 'create').click();
320
+ createNewFolder(folderName);
321
+ }
322
+
323
+ export function renameFolderFromListActions(folderName: string, newName: string) {
324
+ getFolderCard(folderName).click();
325
+ getListActionsToggle().click();
326
+ getListActionItem('rename').click();
327
+ getInlineEditInput().should('be.visible');
328
+ getInlineEditInput().type(`${newName}{enter}`, { delay: 50 });
329
+ successToast().should('exist');
330
+ }
331
+
332
+ export function renameFolderFromCardActions(folderName: string, newName: string) {
333
+ getFolderCardActionToggle(folderName).click();
334
+ getFolderCardActionItem(folderName, 'rename').click();
335
+ renameFolder(newName);
336
+ }
337
+
338
+ export function duplicateWorkflowFromCardActions(workflowName: string, duplicateName: string) {
339
+ getWorkflowCardActions(workflowName).click();
340
+ getWorkflowCardActionItem(workflowName, 'duplicate').click();
341
+ getDuplicateWorkflowModal().find('input').first().type('{selectall}');
342
+ getDuplicateWorkflowModal().find('input').first().type(duplicateName);
343
+ getDuplicateWorkflowModal().find('button').contains('Duplicate').click();
344
+ errorToast().should('not.exist');
345
+ }
346
+
347
+ export function duplicateWorkflowFromWorkflowPage(duplicateName: string) {
348
+ getWorkflowMenu().click();
349
+ cy.getByTestId('workflow-menu-item-duplicate').click();
350
+ getDuplicateWorkflowModal().find('input').first().type('{selectall}');
351
+ getDuplicateWorkflowModal().find('input').first().type(duplicateName);
352
+ getDuplicateWorkflowModal().find('button').contains('Duplicate').click();
353
+ errorToast().should('not.exist');
354
+ }
355
+
356
+ export function deleteEmptyFolderFromCardDropdown(folderName: string) {
357
+ cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder');
358
+ getFolderCard(folderName).click();
359
+ getListActionsToggle().click();
360
+ getListActionItem('delete').click();
361
+ cy.wait('@deleteFolder');
362
+ successToast().should('contain.text', 'Folder deleted');
363
+ }
364
+
365
+ export function deleteEmptyFolderFromListDropdown(folderName: string) {
366
+ cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder');
367
+ getFolderCard(folderName).click();
368
+ getListActionsToggle().click();
369
+ getListActionItem('delete').click();
370
+ cy.wait('@deleteFolder');
371
+ successToast().should('contain.text', 'Folder deleted');
372
+ }
373
+
374
+ export function deleteFolderWithContentsFromListDropdown(folderName: string) {
375
+ getListActionsToggle().click();
376
+ getListActionItem('delete').click();
377
+ confirmFolderDelete(folderName);
378
+ }
379
+
380
+ export function deleteFolderWithContentsFromCardDropdown(folderName: string) {
381
+ getFolderCardActionToggle(folderName).click();
382
+ getFolderCardActionItem(folderName, 'delete').click();
383
+ confirmFolderDelete(folderName);
384
+ }
385
+
386
+ export function deleteAndTransferFolderContentsFromCardDropdown(
387
+ folderName: string,
388
+ destinationName: string,
389
+ ) {
390
+ getFolderCardActionToggle(folderName).click();
391
+ getFolderCardActionItem(folderName, 'delete').click();
392
+ deleteFolderAndMoveContents(folderName, destinationName);
393
+ }
394
+
395
+ export function deleteAndTransferFolderContentsFromListDropdown(destinationName: string) {
396
+ getListActionsToggle().click();
397
+ getListActionItem('delete').click();
398
+ getCurrentBreadcrumbText().then((currentFolderName) => {
399
+ deleteFolderAndMoveContents(String(currentFolderName), destinationName);
400
+ });
401
+ }
402
+
403
+ export function createNewProject(projectName: string, options: { openAfterCreate?: boolean } = {}) {
404
+ cy.getByTestId('universal-add').should('exist').click();
405
+ cy.getByTestId('navigation-menu-item').contains('Project').click();
406
+ cy.getByTestId('project-settings-name-input').type(projectName, { delay: 50 });
407
+ cy.getByTestId('project-settings-save-button').click();
408
+ successToast().should('exist');
409
+ if (options.openAfterCreate) {
410
+ getProjectMenuItem(projectName).click();
411
+ }
412
+ }
413
+
414
+ export function moveFolderFromFolderCardActions(folderName: string, destinationName: string) {
415
+ getFolderCardActionToggle(folderName).click();
416
+ getFolderCardActionItem(folderName, 'move').click();
417
+ moveFolder(folderName, destinationName);
418
+ }
419
+
420
+ export function moveFolderFromListActions(folderName: string, destinationName: string) {
421
+ getFolderCard(folderName).click();
422
+ getListActionsToggle().click();
423
+ getListActionItem('move').click();
424
+ moveFolder(folderName, destinationName);
425
+ }
426
+
427
+ export function moveWorkflowToFolder(workflowName: string, folderName: string) {
428
+ getWorkflowCardActions(workflowName).click();
429
+ getWorkflowCardActionItem(workflowName, 'moveToFolder').click();
430
+ getMoveFolderModal().should('be.visible');
431
+ getMoveToFolderDropdown().click();
432
+ getMoveToFolderInput().type(folderName, { delay: 50 });
433
+ getMoveToFolderOption(folderName).should('be.visible').click();
434
+ getMoveFolderConfirmButton().should('be.enabled').click();
435
+ }
436
+
437
+ export function dragAndDropToFolder(sourceName: string, destinationName: string) {
438
+ const draggable = `[data-test-id=draggable]:has([data-resourcename="${sourceName}"])`;
439
+ const droppable = `[data-test-id=draggable]:has([data-resourcename="${destinationName}"])`;
440
+ cy.get(draggable).trigger('mousedown');
441
+ cy.draganddrop(draggable, droppable, { position: 'center' });
442
+ }
443
+
444
+ export function dragAndDropToProjectRoot(sourceName: string) {
445
+ const draggable = `[data-test-id=draggable]:has([data-resourcename="${sourceName}"])`;
446
+ const droppable = '[data-test-id="home-project"]';
447
+ cy.get(draggable).trigger('mousedown');
448
+ cy.draganddrop(draggable, droppable, { position: 'center' });
449
+ }
450
+
451
+ /**
452
+ * Utils
453
+ */
454
+
455
+ /**
456
+ * Types folder name in the prompt and waits for the folder to be created
457
+ * @param name
458
+ */
459
+ function createNewFolder(name: string) {
460
+ cy.intercept('POST', '/rest/projects/**').as('createFolder');
461
+ cy.get('[role=dialog]')
462
+ .filter(':visible')
463
+ .within(() => {
464
+ cy.get('input.el-input__inner').type(name, { delay: 50 });
465
+ cy.get('button.btn--confirm').click();
466
+ });
467
+ cy.wait('@createFolder');
468
+ successToast().should('exist');
469
+ }
470
+
471
+ function renameFolder(newName: string) {
472
+ cy.intercept('PATCH', '/rest/projects/**').as('renameFolder');
473
+ cy.get('[role=dialog]')
474
+ .filter(':visible')
475
+ .within(() => {
476
+ cy.get('input.el-input__inner').type('{selectall}');
477
+ cy.get('input.el-input__inner').type(newName, { delay: 50 });
478
+ cy.get('button.btn--confirm').click();
479
+ });
480
+ cy.wait('@renameFolder');
481
+ successToast().should('exist');
482
+ }
483
+
484
+ function confirmFolderDelete(folderName: string) {
485
+ cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder');
486
+ getFolderDeleteModal().should('be.visible');
487
+ getDeleteRadioButton().click();
488
+ getConfirmDeleteInput().should('be.visible');
489
+ getConfirmDeleteInput().type(`delete ${folderName}`, { delay: 50 });
490
+ getDeleteFolderModalConfirmButton().should('be.enabled').click();
491
+ cy.wait('@deleteFolder');
492
+ successToast().contains('Folder deleted').should('exist');
493
+ }
494
+
495
+ function deleteFolderAndMoveContents(folderName: string, destinationName: string) {
496
+ cy.intercept('DELETE', '/rest/projects/**').as('deleteFolder');
497
+ getFolderDeleteModal().should('be.visible');
498
+ getFolderDeleteModal().find('h1').first().contains(`Delete "${folderName}"`);
499
+ getTransferContentRadioButton().should('be.visible').click();
500
+ getMoveToFolderDropdown().click();
501
+ getMoveToFolderInput().type(destinationName);
502
+ getMoveToFolderOption(destinationName).click();
503
+ getDeleteFolderModalConfirmButton().should('be.enabled').click();
504
+ cy.wait('@deleteFolder');
505
+ successToast().should('contain.text', `Data transferred to "${destinationName}"`);
506
+ }
507
+
508
+ function moveFolder(folderName: string, destinationName: string) {
509
+ cy.intercept('PATCH', '/rest/projects/**').as('moveFolder');
510
+ getMoveFolderModal().should('be.visible');
511
+ getMoveFolderModal().find('h1').first().contains(`Move folder ${folderName}`);
512
+
513
+ // The dropdown focuses after a small delay (once modal's slide in animation is done).
514
+ // On the component we listen for an event, but here the wait should be very predictable.
515
+ cy.wait(500);
516
+
517
+ // Try to find current folder in the dropdown
518
+ // This tests that auto-focus worked as expected
519
+ cy.focused().type(folderName, { delay: 50 });
520
+ // Should not be available
521
+ getEmptyFolderDropdownMessage('No folders found').should('exist');
522
+ // Select destination folder
523
+ getMoveToFolderInput().type(`{selectall}{backspace}${destinationName}`, {
524
+ delay: 50,
525
+ });
526
+ getMoveToFolderOption(destinationName).should('be.visible').click();
527
+ getMoveFolderConfirmButton().should('be.enabled').click();
528
+ cy.wait('@moveFolder');
529
+ }
530
+
531
+ export function transferWorkflow(
532
+ workflowName: string,
533
+ projectName: string,
534
+ destinationFolder?: string,
535
+ ) {
536
+ getMoveFolderModal().should('be.visible');
537
+ getMoveFolderModal().find('h1').first().contains(`Move workflow ${workflowName}`);
538
+
539
+ cy.wait(500);
540
+
541
+ getProjectSharingInput().should('be.visible').click();
542
+ cy.focused().type(projectName, { delay: 50 });
543
+ getProjectSharingOption(projectName).should('be.visible').click();
544
+
545
+ if (destinationFolder) {
546
+ getMoveToFolderInput().click();
547
+ // Select destination folder
548
+ cy.focused().type(destinationFolder, { delay: 50 });
549
+ getMoveToFolderOption(destinationFolder).should('be.visible').click();
550
+ }
551
+
552
+ getMoveFolderConfirmButton().should('be.enabled').click();
553
+ }
cypress/composables/logs.ts ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Accessors
3
+ */
4
+
5
+ export function getOverviewPanel() {
6
+ return cy.getByTestId('logs-overview');
7
+ }
8
+
9
+ export function getOverviewPanelBody() {
10
+ return cy.getByTestId('logs-overview-body');
11
+ }
12
+
13
+ export function getOverviewStatus() {
14
+ return cy.getByTestId('logs-overview-status');
15
+ }
16
+
17
+ export function getLogEntries() {
18
+ return cy.getByTestId('logs-overview-body').find('[role=treeitem]');
19
+ }
20
+
21
+ export function getSelectedLogEntry() {
22
+ return cy.getByTestId('logs-overview-body').find('[role=treeitem][aria-selected=true]');
23
+ }
24
+
25
+ export function getInputPanel() {
26
+ return cy.getByTestId('log-details-input');
27
+ }
28
+
29
+ export function getInputTableRows() {
30
+ return cy.getByTestId('log-details-input').find('table tr');
31
+ }
32
+
33
+ export function getInputTbodyCell(row: number, col: number) {
34
+ return cy.getByTestId('log-details-input').find('table tr').eq(row).find('td').eq(col);
35
+ }
36
+
37
+ export function getNodeErrorMessageHeader() {
38
+ return cy.getByTestId('log-details-output').findChildByTestId('node-error-message');
39
+ }
40
+
41
+ export function getOutputPanel() {
42
+ return cy.getByTestId('log-details-output');
43
+ }
44
+
45
+ export function getOutputTableRows() {
46
+ return cy.getByTestId('log-details-output').find('table tr');
47
+ }
48
+
49
+ export function getOutputTbodyCell(row: number, col: number) {
50
+ return cy.getByTestId('log-details-output').find('table tr').eq(row).find('td').eq(col);
51
+ }
52
+
53
+ /**
54
+ * Actions
55
+ */
56
+
57
+ export function openLogsPanel() {
58
+ cy.getByTestId('logs-overview-header').click();
59
+ }
60
+
61
+ export function pressClearExecutionButton() {
62
+ cy.getByTestId('logs-overview-header').find('button').contains('Clear execution').click();
63
+ }
64
+
65
+ export function clickLogEntryAtRow(rowIndex: number) {
66
+ getLogEntries().eq(rowIndex).click();
67
+ }
68
+
69
+ export function toggleInputPanel() {
70
+ cy.getByTestId('log-details-header').contains('Input').click();
71
+ }
72
+
73
+ export function clickOpenNdvAtRow(rowIndex: number) {
74
+ getLogEntries().eq(rowIndex).realHover();
75
+ getLogEntries().eq(rowIndex).find('[aria-label="Open..."]').click();
76
+ }
77
+
78
+ export function clickTriggerPartialExecutionAtRow(rowIndex: number) {
79
+ getLogEntries().eq(rowIndex).realHover();
80
+ getLogEntries().eq(rowIndex).find('[aria-label="Execute step"]').click();
81
+ }
82
+
83
+ export function setInputDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema') {
84
+ cy.getByTestId('log-details-input').realHover();
85
+ cy.getByTestId('log-details-input').findChildByTestId(`radio-button-${mode}`).click();
86
+ }
87
+
88
+ export function setOutputDisplayMode(mode: 'table' | 'ai' | 'json' | 'schema') {
89
+ cy.getByTestId('log-details-output').realHover();
90
+ cy.getByTestId('log-details-output').findChildByTestId(`radio-button-${mode}`).click();
91
+ }
cypress/composables/modals/chat-modal.ts ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Getters
3
+ */
4
+
5
+ export function getManualChatModal() {
6
+ return cy.getByTestId('canvas-chat');
7
+ }
8
+
9
+ export function getManualChatInput() {
10
+ return getManualChatModal().get('.chat-inputs textarea');
11
+ }
12
+
13
+ export function getManualChatSendButton() {
14
+ return getManualChatModal().get('.chat-input-send-button');
15
+ }
16
+
17
+ export function getManualChatMessages() {
18
+ return getManualChatModal().get('.chat-messages-list .chat-message');
19
+ }
20
+
21
+ export function getManualChatModalCloseButton() {
22
+ return cy.getByTestId('workflow-chat-button');
23
+ }
24
+
25
+ export function getManualChatDialog() {
26
+ return getManualChatModal().getByTestId('workflow-lm-chat-dialog');
27
+ }
28
+
29
+ /**
30
+ * Actions
31
+ */
32
+
33
+ export function sendManualChatMessage(message: string) {
34
+ getManualChatInput().type(message);
35
+ getManualChatSendButton().click();
36
+ }
37
+
38
+ export function closeManualChatModal() {
39
+ getManualChatModalCloseButton().click();
40
+ }
cypress/composables/modals/credential-modal.ts ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Getters
3
+ */
4
+
5
+ import { clearNotifications } from '../../pages/notifications';
6
+
7
+ export function getCredentialConnectionParameterInputs() {
8
+ return cy.getByTestId('credential-connection-parameter');
9
+ }
10
+
11
+ export function getCredentialConnectionParameterInputByName(name: string) {
12
+ return cy.getByTestId(`parameter-input-${name}`);
13
+ }
14
+
15
+ export function getEditCredentialModal() {
16
+ return cy.getByTestId('editCredential-modal', { timeout: 5000 });
17
+ }
18
+
19
+ export function getCredentialSaveButton() {
20
+ return cy.getByTestId('credential-save-button', { timeout: 5000 });
21
+ }
22
+
23
+ export function getCredentialDeleteButton() {
24
+ return cy.getByTestId('credential-delete-button');
25
+ }
26
+
27
+ export function getCredentialModalCloseButton() {
28
+ return getEditCredentialModal().find('.el-dialog__close').first();
29
+ }
30
+
31
+ /**
32
+ * Actions
33
+ */
34
+
35
+ export function setCredentialConnectionParameterInputByName(name: string, value: string) {
36
+ getCredentialConnectionParameterInputByName(name).type(value);
37
+ }
38
+
39
+ export function saveCredential() {
40
+ getCredentialSaveButton()
41
+ .click({ force: true })
42
+ .within(() => {
43
+ cy.get('button').should('not.exist');
44
+ });
45
+ getCredentialSaveButton().should('have.text', 'Saved');
46
+ }
47
+
48
+ export function closeCredentialModal() {
49
+ getCredentialModalCloseButton().click();
50
+ }
51
+
52
+ export function setCredentialValues(values: Record<string, string>, save = true) {
53
+ Object.entries(values).forEach(([key, value]) => {
54
+ setCredentialConnectionParameterInputByName(key, value);
55
+ });
56
+
57
+ if (save) {
58
+ saveCredential();
59
+ closeCredentialModal();
60
+ clearNotifications();
61
+ }
62
+ }
cypress/composables/modals/save-changes-modal.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export function getSaveChangesModal() {
2
+ return cy.get('.el-overlay').contains('Save changes before leaving?');
3
+ }
4
+
5
+ // this is the button next to 'Save Changes'
6
+ export function getCancelSaveChangesButton() {
7
+ return cy.get('.btn--cancel');
8
+ }
9
+
10
+ // This is the top right 'x'
11
+ export function getCloseSaveChangesButton() {
12
+ return cy.get('.el-message-box__headerbtn');
13
+ }
cypress/composables/modals/workflow-credential-setup-modal.ts ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Getters
3
+ */
4
+
5
+ export const getWorkflowCredentialsModal = () => cy.getByTestId('setup-workflow-credentials-modal');
6
+
7
+ export const getContinueButton = () => cy.getByTestId('continue-button');
8
+
9
+ /**
10
+ * Actions
11
+ */
12
+
13
+ export const closeModalFromContinueButton = () => getContinueButton().click();
cypress/composables/ndv.ts ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Getters
3
+ */
4
+
5
+ import { getVisiblePopper, getVisibleSelect } from '../utils/popper';
6
+
7
+ export function getNdvContainer() {
8
+ return cy.getByTestId('ndv');
9
+ }
10
+
11
+ export function getCredentialSelect(eq = 0) {
12
+ return cy.getByTestId('node-credentials-select').eq(eq);
13
+ }
14
+
15
+ export function getCreateNewCredentialOption() {
16
+ return cy.getByTestId('node-credentials-select-item-new');
17
+ }
18
+
19
+ export function getBackToCanvasButton() {
20
+ return cy.getByTestId('back-to-canvas');
21
+ }
22
+
23
+ export function getExecuteNodeButton() {
24
+ return cy.getByTestId('node-execute-button');
25
+ }
26
+
27
+ export function getParameterInputByName(name: string) {
28
+ return cy.getByTestId(`parameter-input-${name}`);
29
+ }
30
+
31
+ export function getInputPanel() {
32
+ return cy.getByTestId('ndv-input-panel');
33
+ }
34
+
35
+ export function getInputSelect() {
36
+ return cy.getByTestId('ndv-input-select').find('input');
37
+ }
38
+
39
+ export function getInputLinkRun() {
40
+ return getInputPanel().findChildByTestId('link-run');
41
+ }
42
+
43
+ export function getMainPanel() {
44
+ return cy.getByTestId('node-parameters');
45
+ }
46
+
47
+ export function getOutputPanel() {
48
+ return cy.getByTestId('output-panel');
49
+ }
50
+
51
+ export function getFixedCollection(collectionName: string) {
52
+ return cy.getByTestId(`fixed-collection-${collectionName}`);
53
+ }
54
+
55
+ export function getResourceLocator(paramName: string) {
56
+ return cy.getByTestId(`resource-locator-${paramName}`);
57
+ }
58
+
59
+ export function getResourceLocatorInput(paramName: string) {
60
+ return getResourceLocator(paramName).find('[data-test-id="rlc-input-container"]');
61
+ }
62
+
63
+ export function getInputPanelDataContainer() {
64
+ return getInputPanel().findChildByTestId('ndv-data-container');
65
+ }
66
+
67
+ export function getInputTableRows() {
68
+ return getInputPanelDataContainer().find('table tr');
69
+ }
70
+
71
+ export function getInputTbodyCell(row: number, col: number) {
72
+ return getInputTableRows().eq(row).find('td').eq(col);
73
+ }
74
+
75
+ export function getInputRunSelector() {
76
+ return cy.get('[data-test-id="ndv-input-panel"] [data-test-id="run-selector"]');
77
+ }
78
+
79
+ export function getInputPanelItemsCount() {
80
+ return getInputPanel().getByTestId('ndv-items-count');
81
+ }
82
+
83
+ export function getOutputPanelDataContainer() {
84
+ return getOutputPanel().findChildByTestId('ndv-data-container');
85
+ }
86
+
87
+ export function getOutputTableRows() {
88
+ return getOutputPanelDataContainer().find('table tr');
89
+ }
90
+
91
+ export function getOutputTableRow(row: number) {
92
+ return getOutputTableRows().eq(row);
93
+ }
94
+
95
+ export function getOutputTableHeaders() {
96
+ return getOutputPanelDataContainer().find('table thead th');
97
+ }
98
+
99
+ export function getOutputTableHeaderByText(text: string) {
100
+ return getOutputTableHeaders().contains(text);
101
+ }
102
+
103
+ export function getOutputTbodyCell(row: number, col: number) {
104
+ return getOutputTableRows().eq(row).find('td').eq(col);
105
+ }
106
+
107
+ export function getOutputRunSelector() {
108
+ return cy.get('[data-test-id="output-panel"] [data-test-id="run-selector"]');
109
+ }
110
+
111
+ export function getOutputRunSelectorInput() {
112
+ return getOutputRunSelector().find('input');
113
+ }
114
+
115
+ export function getOutputPanelTable() {
116
+ return getOutputPanelDataContainer().get('table');
117
+ }
118
+
119
+ export function getRunDataInfoCallout() {
120
+ return cy.getByTestId('run-data-callout');
121
+ }
122
+
123
+ export function getOutputPanelItemsCount() {
124
+ return getOutputPanel().getByTestId('ndv-items-count');
125
+ }
126
+
127
+ export function getOutputPanelRelatedExecutionLink() {
128
+ return getOutputPanel().getByTestId('related-execution-link');
129
+ }
130
+
131
+ export function getNodeOutputHint() {
132
+ return cy.getByTestId('ndv-output-run-node-hint');
133
+ }
134
+
135
+ export function getWorkflowCards() {
136
+ return cy.getByTestId('resources-list-item-workflow');
137
+ }
138
+
139
+ export function getWorkflowCard(workflowName: string) {
140
+ return getWorkflowCards()
141
+ .contains(workflowName)
142
+ .parents('[data-test-id="resources-list-item-workflow"]');
143
+ }
144
+
145
+ export function getWorkflowCardContent(workflowName: string) {
146
+ return getWorkflowCard(workflowName).findChildByTestId('card-content');
147
+ }
148
+
149
+ export function getNodeRunInfoStale() {
150
+ return cy.getByTestId('node-run-info-stale');
151
+ }
152
+
153
+ export function getNodeOutputErrorMessage() {
154
+ return getOutputPanel().findChildByTestId('node-error-message');
155
+ }
156
+
157
+ export function getParameterExpressionPreviewValue() {
158
+ return cy.getByTestId('parameter-expression-preview-value');
159
+ }
160
+
161
+ /**
162
+ * Actions
163
+ */
164
+
165
+ export function openCredentialSelect(eq = 0) {
166
+ getCredentialSelect(eq).click();
167
+ }
168
+
169
+ export function setCredentialByName(name: string) {
170
+ openCredentialSelect();
171
+ getCredentialSelect().contains(name).click();
172
+ }
173
+
174
+ export function clickCreateNewCredential() {
175
+ openCredentialSelect();
176
+ getCreateNewCredentialOption().click({ force: true });
177
+ }
178
+
179
+ export function clickGetBackToCanvas() {
180
+ getBackToCanvasButton().click();
181
+ }
182
+
183
+ export function clickExecuteNode() {
184
+ getExecuteNodeButton().click();
185
+ }
186
+
187
+ export function clickResourceLocatorInput(paramName: string) {
188
+ getResourceLocatorInput(paramName).click();
189
+ }
190
+
191
+ export function setParameterInputByName(name: string, value: string) {
192
+ getParameterInputByName(name).clear().type(value);
193
+ }
194
+
195
+ export function checkParameterCheckboxInputByName(name: string) {
196
+ getParameterInputByName(name).find('input[type="checkbox"]').check({ force: true });
197
+ }
198
+
199
+ export function uncheckParameterCheckboxInputByName(name: string) {
200
+ getParameterInputByName(name).find('input[type="checkbox"]').uncheck({ force: true });
201
+ }
202
+
203
+ export function setParameterSelectByContent(name: string, content: string) {
204
+ getParameterInputByName(name).realClick();
205
+ getVisibleSelect().find('.option-headline').contains(content).click();
206
+ }
207
+
208
+ export function changeOutputRunSelector(runName: string) {
209
+ getOutputRunSelector().click();
210
+ getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
211
+ }
212
+
213
+ export function addItemToFixedCollection(collectionName: string) {
214
+ getFixedCollection(collectionName).getByTestId('fixed-collection-add').click();
215
+ }
216
+
217
+ export function typeIntoFixedCollectionItem(collectionName: string, index: number, value: string) {
218
+ getFixedCollection(collectionName).within(() =>
219
+ cy.getByTestId('parameter-input').eq(index).type(value),
220
+ );
221
+ }
222
+
223
+ export function selectResourceLocatorAddResourceItem(
224
+ resourceLocator: string,
225
+ expectedText: string,
226
+ ) {
227
+ clickResourceLocatorInput(resourceLocator);
228
+
229
+ // getVisiblePopper().findChildByTestId('rlc-item-add-resource').eq(0).should('exist');
230
+ getVisiblePopper()
231
+ .findChildByTestId('rlc-item-add-resource')
232
+ .eq(0)
233
+ .find('span')
234
+ .should('contain.text', expectedText)
235
+ .click();
236
+ }
237
+
238
+ export function selectResourceLocatorItem(
239
+ resourceLocator: string,
240
+ index: number,
241
+ expectedText: string,
242
+ ) {
243
+ clickResourceLocatorInput(resourceLocator);
244
+
245
+ getVisiblePopper().findChildByTestId('rlc-item').eq(0).should('exist');
246
+ getVisiblePopper()
247
+ .findChildByTestId('rlc-item')
248
+ .eq(index)
249
+ .find('span')
250
+ .should('contain.text', expectedText)
251
+ .click();
252
+ }
253
+
254
+ export function clickWorkflowCardContent(workflowName: string) {
255
+ getWorkflowCardContent(workflowName).click();
256
+ }
257
+
258
+ export function clickAssignmentCollectionAdd() {
259
+ cy.getByTestId('assignment-collection-drop-area').click();
260
+ }
261
+
262
+ export function assertNodeOutputHintExists() {
263
+ getNodeOutputHint().should('exist');
264
+ }
265
+
266
+ export function assertNodeOutputErrorMessageExists() {
267
+ return getNodeOutputErrorMessage().should('exist');
268
+ }
269
+
270
+ // Note that this only validates the expectedContent is *included* in the output table
271
+ export function assertOutputTableContent(expectedContent: unknown[][]) {
272
+ for (const [i, row] of expectedContent.entries()) {
273
+ for (const [j, value] of row.entries()) {
274
+ // + 1 to skip header
275
+ getOutputTbodyCell(1 + i, j).should('have.text', value);
276
+ }
277
+ }
278
+ }
279
+
280
+ export function populateMapperFields(fields: ReadonlyArray<[string, string]>) {
281
+ for (const [name, value] of fields) {
282
+ getParameterInputByName(name).type(value);
283
+
284
+ // Click on a parent to dismiss the pop up which hides the field below.
285
+ getParameterInputByName(name).parent().parent().parent().parent().click('topLeft');
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Populate multiValue fixedCollections. Only supports fixedCollections for which all fields can be defined via keyboard typing
291
+ *
292
+ * @param items - 2D array of items to populate, i.e. [["myField1", "String"], ["myField2", "Number"]]
293
+ * @param collectionName - name of the fixedCollection to populate
294
+ * @param offset - amount of 'parameter-input's before start, e.g. from a controlling dropdown that makes the fields appear
295
+ * @returns
296
+ */
297
+ export function populateFixedCollection<T extends readonly string[]>(
298
+ items: readonly T[],
299
+ collectionName: string,
300
+ offset: number = 0,
301
+ ) {
302
+ if (items.length === 0) return;
303
+ const n = items[0].length;
304
+ for (const [i, params] of items.entries()) {
305
+ addItemToFixedCollection(collectionName);
306
+ for (const [j, param] of params.entries()) {
307
+ getFixedCollection(collectionName)
308
+ .getByTestId('parameter-input')
309
+ .eq(offset + i * n + j)
310
+ .type(`${param}{downArrow}{enter}`);
311
+ }
312
+ }
313
+ }
314
+
315
+ export function assertInlineExpressionValid() {
316
+ cy.getByTestId('inline-expression-editor-input').find('.cm-valid-resolvable').should('exist');
317
+ }
318
+
319
+ export function hoverInputItemByText(text: string) {
320
+ return getInputPanelDataContainer().contains(text).realHover();
321
+ }
322
+
323
+ export function verifyInputHoverState(expectedText: string) {
324
+ getInputPanelDataContainer()
325
+ .find('[data-test-id="hovering-item"]')
326
+ .should('be.visible')
327
+ .should('have.text', expectedText);
328
+ }
329
+
330
+ export function verifyOutputHoverState(expectedText: string) {
331
+ getOutputPanelDataContainer()
332
+ .find('[data-test-id="hovering-item"]')
333
+ .should('be.visible')
334
+ .should('have.text', expectedText);
335
+ }
336
+
337
+ export function resetHoverState() {
338
+ getBackToCanvasButton().realHover();
339
+ }
340
+
341
+ export function setInputDisplayMode(mode: 'Schema' | 'Table' | 'JSON' | 'Binary') {
342
+ getInputPanel().findChildByTestId('ndv-run-data-display-mode').contains(mode).click();
343
+ }
344
+
345
+ export function toggleInputRunLinking() {
346
+ getInputLinkRun().click();
347
+ }