diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index c38f038105..8f256de286 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -109,7 +109,6 @@ jobs: context: ./docker/images/n8n build-args: | N8N_VERSION=${{ needs.publish-to-npm.outputs.release }} - N8N_RELEASE_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") platforms: linux/amd64,linux/arm64 provenance: false push: true diff --git a/docker/images/n8n/Dockerfile b/docker/images/n8n/Dockerfile index e81732dda2..9cc8ab7216 100644 --- a/docker/images/n8n/Dockerfile +++ b/docker/images/n8n/Dockerfile @@ -4,18 +4,15 @@ FROM n8nio/base:${NODE_VERSION} ARG N8N_VERSION RUN if [ -z "$N8N_VERSION" ] ; then echo "The N8N_VERSION argument is missing!" ; exit 1; fi -ARG N8N_RELEASE_DATE LABEL org.opencontainers.image.title="n8n" LABEL org.opencontainers.image.description="Workflow Automation Tool" LABEL org.opencontainers.image.source="https://github.com/n8n-io/n8n" LABEL org.opencontainers.image.url="https://n8n.io" LABEL org.opencontainers.image.version=${N8N_VERSION} -LABEL org.opencontainers.image.created=${N8N_RELEASE_DATE} ENV N8N_VERSION=${N8N_VERSION} ENV NODE_ENV=production ENV N8N_RELEASE_TYPE=stable -ENV N8N_RELEASE_DATE=${N8N_RELEASE_DATE} RUN set -eux; \ npm install -g --omit=dev n8n@${N8N_VERSION} --ignore-scripts && \ npm rebuild --prefix=/usr/local/lib/node_modules/n8n sqlite3 && \ diff --git a/packages/@n8n/config/src/configs/generic.config.ts b/packages/@n8n/config/src/configs/generic.config.ts index 133d62ee29..f6960b2415 100644 --- a/packages/@n8n/config/src/configs/generic.config.ts +++ b/packages/@n8n/config/src/configs/generic.config.ts @@ -9,9 +9,6 @@ export class GenericConfig { @Env('N8N_RELEASE_TYPE') releaseChannel: 'stable' | 'beta' | 'nightly' | 'dev' = 'dev'; - @Env('N8N_RELEASE_DATE') - releaseDate?: Date; - /** Grace period (in seconds) to wait for components to shut down before process exit. */ @Env('N8N_GRACEFUL_SHUTDOWN_TIMEOUT') gracefulShutdownTimeout: number = 30; diff --git a/packages/@n8n/config/src/configs/sentry.config.ts b/packages/@n8n/config/src/configs/sentry.config.ts index 97e34edeea..ac4c805c2b 100644 --- a/packages/@n8n/config/src/configs/sentry.config.ts +++ b/packages/@n8n/config/src/configs/sentry.config.ts @@ -10,14 +10,6 @@ export class SentryConfig { @Env('N8N_FRONTEND_SENTRY_DSN') frontendDsn: string = ''; - /** - * Version of the n8n instance - * - * @example '1.73.0' - */ - @Env('N8N_VERSION') - n8nVersion: string = ''; - /** * Environment of the n8n instance. * diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 626c5f4d62..8a962b437f 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -243,7 +243,6 @@ describe('GlobalConfig', () => { sentry: { backendDsn: '', frontendDsn: '', - n8nVersion: '', environment: '', deploymentName: '', }, @@ -326,7 +325,6 @@ describe('GlobalConfig', () => { DB_LOGGING_MAX_EXECUTION_TIME: '0', N8N_METRICS: 'TRUE', N8N_TEMPLATES_ENABLED: '0', - N8N_RELEASE_DATE: '2025-02-17T13:54:15Z', }; const config = Container.get(GlobalConfig); expect(structuredClone(config)).toEqual({ @@ -358,10 +356,6 @@ describe('GlobalConfig', () => { ...defaultConfig.templates, enabled: false, }, - generic: { - ...defaultConfig.generic, - releaseDate: new Date('2025-02-17T13:54:15.000Z'), - }, }); expect(mockFs.readFileSync).not.toHaveBeenCalled(); }); @@ -397,15 +391,4 @@ describe('GlobalConfig', () => { 'Invalid number value for DB_LOGGING_MAX_EXECUTION_TIME: abcd', ); }); - - it('should handle invalid timestamps', () => { - process.env = { - N8N_RELEASE_DATE: 'abcd', - }; - const config = Container.get(GlobalConfig); - expect(config.generic.releaseDate).toBeUndefined(); - expect(consoleWarnMock).toHaveBeenCalledWith( - 'Invalid timestamp value for N8N_RELEASE_DATE: abcd', - ); - }); }); diff --git a/packages/cli/src/commands/base-command.ts b/packages/cli/src/commands/base-command.ts index d5b9427ed2..c5374dc299 100644 --- a/packages/cli/src/commands/base-command.ts +++ b/packages/cli/src/commands/base-command.ts @@ -14,7 +14,13 @@ import { ensureError, sleep, UserError } from 'n8n-workflow'; import type { AbstractServer } from '@/abstract-server'; import config from '@/config'; -import { LICENSE_FEATURES, inDevelopment, inTest } from '@/constants'; +import { + LICENSE_FEATURES, + N8N_VERSION, + N8N_RELEASE_DATE, + inDevelopment, + inTest, +} from '@/constants'; import * as CrashJournal from '@/crash-journal'; import * as Db from '@/db'; import { getDataDeduplicationService } from '@/deduplication'; @@ -63,15 +69,14 @@ export abstract class BaseCommand extends Command { async init(): Promise { this.errorReporter = Container.get(ErrorReporter); - const { releaseDate } = this.globalConfig.generic; - const { backendDsn, n8nVersion, environment, deploymentName } = this.globalConfig.sentry; + const { backendDsn, environment, deploymentName } = this.globalConfig.sentry; await this.errorReporter.init({ serverType: this.instanceSettings.instanceType, dsn: backendDsn, environment, - release: n8nVersion, + release: N8N_VERSION, serverName: deploymentName, - releaseDate, + releaseDate: N8N_RELEASE_DATE, }); initExpressionEvaluator(); diff --git a/packages/cli/src/constants.ts b/packages/cli/src/constants.ts index abcf298d3d..bddf414515 100644 --- a/packages/cli/src/constants.ts +++ b/packages/cli/src/constants.ts @@ -1,4 +1,4 @@ -import { readFileSync } from 'fs'; +import { readFileSync, statSync } from 'fs'; import type { n8n } from 'n8n-core'; import type { ITaskDataConnections } from 'n8n-workflow'; import { jsonParse, TRIMMED_TASK_DATA_CONNECTIONS_KEY } from 'n8n-workflow'; @@ -18,9 +18,10 @@ export const TEMPLATES_DIR = join(CLI_DIR, 'templates'); export const NODES_BASE_DIR = dirname(require.resolve('n8n-nodes-base')); export const EDITOR_UI_DIST_DIR = join(dirname(require.resolve('n8n-editor-ui')), 'dist'); -export function getN8nPackageJson() { - return jsonParse(readFileSync(join(CLI_DIR, 'package.json'), 'utf8')); -} +const packageJsonPath = join(CLI_DIR, 'package.json'); +const n8nPackageJson = jsonParse(readFileSync(packageJsonPath, 'utf8')); +export const N8N_VERSION = n8nPackageJson.version; +export const N8N_RELEASE_DATE = statSync(packageJsonPath).mtime; export const STARTING_NODES = [ '@n8n/n8n-nodes-langchain.manualChatTrigger', @@ -28,8 +29,6 @@ export const STARTING_NODES = [ 'n8n-nodes-base.manualTrigger', ]; -export const N8N_VERSION = getN8nPackageJson().version; - export const NODE_PACKAGE_PREFIX = 'n8n-nodes-'; export const STARTER_TEMPLATE_NAME = `${NODE_PACKAGE_PREFIX}starter`; diff --git a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts index 0f35230be4..43064daca4 100644 --- a/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts +++ b/packages/cli/src/security-audit/risk-reporters/instance-risk-reporter.ts @@ -5,7 +5,7 @@ import { InstanceSettings, Logger } from 'n8n-core'; import type { IWorkflowBase } from 'n8n-workflow'; import config from '@/config'; -import { getN8nPackageJson, inDevelopment } from '@/constants'; +import { inDevelopment, N8N_VERSION } from '@/constants'; import { isApiEnabled } from '@/public-api'; import { ENV_VARS_DOCS_URL, @@ -175,7 +175,7 @@ export class InstanceRiskReporter implements RiskReporter { private async getOutdatedState() { let versions = []; - const localVersion = getN8nPackageJson().version; + const localVersion = N8N_VERSION; try { versions = await this.getNextVersions(localVersion).then((v) => this.removeIconData(v)); diff --git a/packages/cli/test/integration/security-audit/utils.ts b/packages/cli/test/integration/security-audit/utils.ts index d153db28eb..9206ab51c9 100644 --- a/packages/cli/test/integration/security-audit/utils.ts +++ b/packages/cli/test/integration/security-audit/utils.ts @@ -114,9 +114,8 @@ export const MOCK_PACKAGE: InstalledPackages[] = [ export function simulateOutdatedInstanceOnce(versionName = MOCK_01110_N8N_VERSION.name) { const baseUrl = Container.get(GlobalConfig).versionNotifications.endpoint + '/'; - jest - .spyOn(constants, 'getN8nPackageJson') - .mockReturnValueOnce({ name: 'n8n', version: versionName }); + // @ts-expect-error readonly export + constants.N8N_VERSION = versionName; nock(baseUrl).get(versionName).reply(200, [MOCK_01110_N8N_VERSION, MOCK_09990_N8N_VERSION]); } @@ -124,9 +123,8 @@ export function simulateOutdatedInstanceOnce(versionName = MOCK_01110_N8N_VERSIO export function simulateUpToDateInstance(versionName = MOCK_09990_N8N_VERSION.name) { const baseUrl = Container.get(GlobalConfig).versionNotifications.endpoint + '/'; - jest - .spyOn(constants, 'getN8nPackageJson') - .mockReturnValueOnce({ name: 'n8n', version: versionName }); + // @ts-expect-error readonly export + constants.N8N_VERSION = versionName; nock(baseUrl).persist().get(versionName).reply(200, [MOCK_09990_N8N_VERSION]); } diff --git a/packages/core/src/errors/error-reporter.ts b/packages/core/src/errors/error-reporter.ts index f2c99bffbc..50e900cce8 100644 --- a/packages/core/src/errors/error-reporter.ts +++ b/packages/core/src/errors/error-reporter.ts @@ -24,7 +24,8 @@ type ErrorReporterInitOptions = { beforeSendFilter?: (event: ErrorEvent, hint: EventHint) => boolean; }; -const SIX_WEEKS_IN_MS = 6 * 7 * 24 * 60 * 60 * 1000; +const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000; +const SIX_WEEKS_IN_MS = 6 * 7 * ONE_DAY_IN_MS; const RELEASE_EXPIRATION_WARNING = 'Error tracking disabled because this release is older than 6 weeks.'; @@ -86,17 +87,23 @@ export class ErrorReporter { }); if (releaseDate) { - const releaseExpiresInMs = releaseDate.getTime() + SIX_WEEKS_IN_MS - Date.now(); - if (releaseExpiresInMs <= 0) { + const releaseExpiresAtMs = releaseDate.getTime() + SIX_WEEKS_IN_MS; + const releaseExpiresInMs = () => releaseExpiresAtMs - Date.now(); + if (releaseExpiresInMs() <= 0) { this.logger.warn(RELEASE_EXPIRATION_WARNING); return; } - // Once this release expires, reject all events - this.expirationTimer = setTimeout(() => { - this.logger.warn(RELEASE_EXPIRATION_WARNING); - // eslint-disable-next-line @typescript-eslint/unbound-method - this.report = this.defaultReport; - }, releaseExpiresInMs); + const checkForExpiration = () => { + // Once this release expires, reject all events + if (releaseExpiresInMs() <= 0) { + this.logger.warn(RELEASE_EXPIRATION_WARNING); + // eslint-disable-next-line @typescript-eslint/unbound-method + this.report = this.defaultReport; + } else { + setTimeout(checkForExpiration, ONE_DAY_IN_MS); + } + }; + checkForExpiration(); } if (!dsn) return; diff --git a/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts new file mode 100644 index 0000000000..b9d1ae9eb9 --- /dev/null +++ b/packages/core/src/execution-engine/partial-execution-utils/__tests__/find-trigger-for-partial-execution.test.ts @@ -0,0 +1,217 @@ +import { mock } from 'jest-mock-extended'; +import type { IConnections, INode, INodeType, INodeTypes, IPinData } from 'n8n-workflow'; +import { Workflow } from 'n8n-workflow'; + +import { toIConnections } from './helpers'; +import { findTriggerForPartialExecution } from '../find-trigger-for-partial-execution'; + +describe('findTriggerForPartialExecution', () => { + const nodeTypes = mock(); + + const createMockWorkflow = (nodes: INode[], connections: IConnections, pinData?: IPinData) => + new Workflow({ + active: false, + nodes, + connections, + nodeTypes, + pinData, + }); + + const createNode = (name: string, type: string, disabled = false) => + mock({ name, type, disabled }); + const manualTriggerNode = createNode('ManualTrigger', 'n8n-nodes-base.manualTrigger'); + const disabledTriggerNode = createNode('DisabledTrigger', 'n8n-nodes-base.manualTrigger', true); + const pinnedTrigger = createNode('PinnedTrigger', 'n8n-nodes-base.manualTrigger'); + const setNode = createNode('Set', 'n8n-nodes-base.set'); + const noOpNode = createNode('No Operation', 'n8n-nodes-base.noOp'); + const webhookNode = createNode('Webhook', 'n8n-nodes-base.webhook'); + const webhookNode1 = createNode('Webhook1', 'n8n-nodes-base.webhook'); + + beforeEach(() => { + nodeTypes.getByNameAndVersion.mockImplementation((type) => { + const isTrigger = type.endsWith('Trigger') || type.endsWith('webhook'); + return mock({ + description: { + group: isTrigger ? ['trigger'] : [], + properties: [], + }, + }); + }); + }); + + const testGroups: Record< + string, + Array<{ + description: string; + nodes: INode[]; + connections: Array<{ to: INode; from: INode }>; + destinationNodeName: string; + pinData?: IPinData; + expectedTrigger?: INode; + }> + > = { + 'Single trigger node': [ + { + description: 'should return the destination node if it is a trigger', + nodes: [manualTriggerNode], + connections: [], + destinationNodeName: manualTriggerNode.name, + expectedTrigger: manualTriggerNode, + }, + { + description: 'should return a parent trigger node for a non-trigger destination', + nodes: [manualTriggerNode, setNode], + connections: [{ from: manualTriggerNode, to: setNode }], + destinationNodeName: setNode.name, + expectedTrigger: manualTriggerNode, + }, + ], + 'Multiple trigger nodes': [ + { + description: 'should prioritize webhook nodes when multiple parent triggers exist', + nodes: [webhookNode, manualTriggerNode, setNode], + connections: [ + { from: webhookNode, to: setNode }, + { from: manualTriggerNode, to: setNode }, + ], + destinationNodeName: setNode.name, + expectedTrigger: webhookNode, + }, + { + description: 'should handle multiple webhook triggers', + nodes: [webhookNode, webhookNode1, setNode], + connections: [ + { from: webhookNode, to: setNode }, + { from: webhookNode1, to: setNode }, + ], + destinationNodeName: setNode.name, + expectedTrigger: webhookNode1, + }, + { + description: 'should prioritize webhook node, even if it is further up', + nodes: [manualTriggerNode, setNode, noOpNode, webhookNode], + connections: [ + { from: manualTriggerNode, to: setNode }, + { from: setNode, to: noOpNode }, + { from: webhookNode, to: noOpNode }, + ], + destinationNodeName: noOpNode.name, + expectedTrigger: webhookNode, + }, + { + description: 'should ignore disabled parent trigger nodes', + nodes: [disabledTriggerNode, manualTriggerNode, setNode], + connections: [ + { from: disabledTriggerNode, to: setNode }, + { from: manualTriggerNode, to: setNode }, + ], + destinationNodeName: setNode.name, + expectedTrigger: manualTriggerNode, + }, + ], + 'No trigger nodes': [ + { + description: 'should return undefined when no valid parent triggers found', + nodes: [setNode, noOpNode], + connections: [{ from: setNode, to: noOpNode }], + destinationNodeName: noOpNode.name, + expectedTrigger: undefined, + }, + ], + 'Trigger node with pinned data': [ + { + description: 'should prioritize pinned trigger nodes', + nodes: [pinnedTrigger, manualTriggerNode, setNode], + connections: [ + { from: pinnedTrigger, to: setNode }, + { from: manualTriggerNode, to: setNode }, + ], + destinationNodeName: setNode.name, + pinData: { [pinnedTrigger.name]: [{ json: { test: true } }] }, + expectedTrigger: pinnedTrigger, + }, + { + description: 'should prioritize pinned webhook triggers', + nodes: [pinnedTrigger, manualTriggerNode, webhookNode, setNode], + connections: [ + { from: pinnedTrigger, to: setNode }, + { from: webhookNode, to: setNode }, + { from: manualTriggerNode, to: setNode }, + ], + destinationNodeName: setNode.name, + pinData: { + [pinnedTrigger.name]: [{ json: { test: true } }], + [webhookNode.name]: [{ json: { test: true } }], + }, + expectedTrigger: webhookNode, + }, + { + description: 'should prioritize the first connected pinned webhook triggers', + nodes: [webhookNode, webhookNode1, pinnedTrigger, manualTriggerNode, setNode], + connections: [ + { from: pinnedTrigger, to: setNode }, + { from: webhookNode, to: setNode }, + { from: webhookNode1, to: setNode }, + { from: manualTriggerNode, to: setNode }, + ], + destinationNodeName: setNode.name, + pinData: { + [pinnedTrigger.name]: [{ json: { test: true } }], + [webhookNode.name]: [{ json: { test: true } }], + [webhookNode1.name]: [{ json: { test: true } }], + }, + expectedTrigger: webhookNode, + }, + { + description: 'should prioritize the first connected pinned webhook triggers (reverse)', + nodes: [webhookNode1, webhookNode, pinnedTrigger, manualTriggerNode, setNode], + connections: [ + { from: pinnedTrigger, to: setNode }, + { from: webhookNode1, to: setNode }, + { from: webhookNode, to: setNode }, + { from: manualTriggerNode, to: setNode }, + ], + destinationNodeName: setNode.name, + pinData: { + [pinnedTrigger.name]: [{ json: { test: true } }], + [webhookNode.name]: [{ json: { test: true } }], + [webhookNode1.name]: [{ json: { test: true } }], + }, + expectedTrigger: webhookNode1, + }, + ], + }; + + for (const [group, tests] of Object.entries(testGroups)) { + describe(group, () => { + test.each(tests)( + '$description', + ({ nodes, connections, destinationNodeName, expectedTrigger, pinData }) => { + const workflow = createMockWorkflow(nodes, toIConnections(connections), pinData); + expect(findTriggerForPartialExecution(workflow, destinationNodeName)).toBe( + expectedTrigger, + ); + }, + ); + }); + } + + describe('Error and Edge Case Handling', () => { + it('should handle non-existent destination node gracefully', () => { + const workflow = createMockWorkflow([], {}); + expect(findTriggerForPartialExecution(workflow, 'NonExistentNode')).toBeUndefined(); + }); + + it('should handle empty workflow', () => { + const workflow = createMockWorkflow([], {}); + expect(findTriggerForPartialExecution(workflow, '')).toBeUndefined(); + }); + + it('should handle workflow with no connections', () => { + const workflow = createMockWorkflow([manualTriggerNode], {}); + expect(findTriggerForPartialExecution(workflow, manualTriggerNode.name)).toBe( + manualTriggerNode, + ); + }); + }); +}); diff --git a/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts b/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts index 977e99c107..a788d958c9 100644 --- a/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts +++ b/packages/core/src/execution-engine/partial-execution-utils/find-trigger-for-partial-execution.ts @@ -1,5 +1,7 @@ import * as assert from 'assert/strict'; -import type { INode, Workflow } from 'n8n-workflow'; +import type { INode, INodeType, Workflow } from 'n8n-workflow'; + +const isTriggerNode = (nodeType: INodeType) => nodeType.description.group.includes('trigger'); function findAllParentTriggers(workflow: Workflow, destinationNodeName: string) { const parentNodes = workflow @@ -17,35 +19,50 @@ function findAllParentTriggers(workflow: Workflow, destinationNodeName: string) }; }) .filter((value) => value !== null) - .filter(({ nodeType }) => nodeType.description.group.includes('trigger')) + .filter(({ nodeType }) => isTriggerNode(nodeType)) .map(({ node }) => node); return parentNodes; } -// TODO: write unit tests for this // TODO: rewrite this using DirectedGraph instead of workflow. export function findTriggerForPartialExecution( workflow: Workflow, destinationNodeName: string, ): INode | undefined { + // First, check if the destination node itself is a trigger + const destinationNode = workflow.getNode(destinationNodeName); + if (!destinationNode) return; + + const destinationNodeType = workflow.nodeTypes.getByNameAndVersion( + destinationNode.type, + destinationNode.typeVersion, + ); + + if (isTriggerNode(destinationNodeType) && !destinationNode.disabled) { + return destinationNode; + } + + // Since the destination node wasn't a trigger, we try to find a parent node that's a trigger const parentTriggers = findAllParentTriggers(workflow, destinationNodeName).filter( (trigger) => !trigger.disabled, ); + + // Prioritize webhook triggers with pinned-data const pinnedTriggers = parentTriggers // TODO: add the other filters here from `findAllPinnedActivators`, see // copy below. .filter((trigger) => workflow.pinData?.[trigger.name]) - // TODO: Make this sorting more predictable // Put nodes which names end with 'webhook' first, while also reversing the // order they had in the original array. - .sort((n) => (n.type.endsWith('webhook') ? -1 : 1)); - + .sort((a, b) => (a.type.endsWith('webhook') ? -1 : b.type.endsWith('webhook') ? 1 : 0)); if (pinnedTriggers.length) { return pinnedTriggers[0]; - } else { - return parentTriggers[0]; } + + // Prioritize webhook triggers over other parent triggers + const webhookTriggers = parentTriggers.filter((trigger) => trigger.type.endsWith('webhook')); + return webhookTriggers.length > 0 ? webhookTriggers[0] : parentTriggers[0]; } //function findAllPinnedActivators(workflow: Workflow, pinData?: IPinData) { diff --git a/packages/frontend/editor-ui/package.json b/packages/frontend/editor-ui/package.json index 227efb76b4..e1e632d887 100644 --- a/packages/frontend/editor-ui/package.json +++ b/packages/frontend/editor-ui/package.json @@ -94,11 +94,11 @@ "xss": "catalog:" }, "devDependencies": { + "@faker-js/faker": "^8.0.2", + "@iconify/json": "^2.2.228", "@n8n/eslint-config": "workspace:*", "@n8n/typescript-config": "workspace:*", "@n8n/vitest-config": "workspace:*", - "@faker-js/faker": "^8.0.2", - "@iconify/json": "^2.2.228", "@pinia/testing": "^0.1.6", "@types/dateformat": "^3.0.0", "@types/file-saver": "^2.0.1", @@ -111,6 +111,7 @@ "@vitejs/plugin-vue": "catalog:frontend", "@vitest/coverage-v8": "catalog:frontend", "browserslist-to-esbuild": "^2.1.1", + "fake-indexeddb": "^6.0.0", "miragejs": "^0.1.48", "unplugin-icons": "^0.19.0", "unplugin-vue-components": "^0.27.2", diff --git a/packages/frontend/editor-ui/src/__tests__/setup.ts b/packages/frontend/editor-ui/src/__tests__/setup.ts index 934017e2ba..0e268a87d2 100644 --- a/packages/frontend/editor-ui/src/__tests__/setup.ts +++ b/packages/frontend/editor-ui/src/__tests__/setup.ts @@ -1,4 +1,5 @@ import '@testing-library/jest-dom'; +import 'fake-indexeddb/auto'; import { configure } from '@testing-library/vue'; import 'core-js/proposals/set-methods-v2'; @@ -64,20 +65,21 @@ Object.defineProperty(window, 'matchMedia', { }); class Worker { - onmessage: (message: string) => void; + onmessage = vi.fn(); url: string; constructor(url: string) { this.url = url; - this.onmessage = () => {}; } - postMessage(message: string) { + postMessage = vi.fn((message: string) => { this.onmessage(message); - } + }); - addEventListener() {} + addEventListener = vi.fn(); + + terminate = vi.fn(); } Object.defineProperty(window, 'Worker', { diff --git a/packages/frontend/editor-ui/src/components/ApiKeyCreateOrEditModal.vue b/packages/frontend/editor-ui/src/components/ApiKeyCreateOrEditModal.vue index 83820e756b..381462bc90 100644 --- a/packages/frontend/editor-ui/src/components/ApiKeyCreateOrEditModal.vue +++ b/packages/frontend/editor-ui/src/components/ApiKeyCreateOrEditModal.vue @@ -198,6 +198,16 @@ const onSelect = (value: number) => { expirationDate.value = ''; showExpirationDateSelector.value = false; }; + +async function handleEnterKey(event: KeyboardEvent) { + if (event.key === 'Enter') { + if (props.mode === 'new') { + await onSave(); + } else { + await onEdit(); + } + } +}