From 73daabbd0ea860bb962e3fcc84f2a607f5a23b34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20Ovejero?= Date: Tue, 24 Sep 2024 11:02:39 +0200 Subject: [PATCH 01/29] docs(core): Document access checks (#10929) --- .../src/credentials/credentials.service.ts | 4 +- .../cli/src/decorators/controller.registry.ts | 4 +- packages/cli/src/permissions/check-access.ts | 75 ++++++++----------- .../shared/middlewares/global.middleware.ts | 4 +- 4 files changed, 38 insertions(+), 49 deletions(-) diff --git a/packages/cli/src/credentials/credentials.service.ts b/packages/cli/src/credentials/credentials.service.ts index 43041a84b1..dc5ab4e6c7 100644 --- a/packages/cli/src/credentials/credentials.service.ts +++ b/packages/cli/src/credentials/credentials.service.ts @@ -34,7 +34,7 @@ import { ExternalHooks } from '@/external-hooks'; import { validateEntity } from '@/generic-helpers'; import type { ICredentialsDb } from '@/interfaces'; import { Logger } from '@/logger'; -import { userHasScope } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions/check-access'; import type { CredentialRequest, ListQuery } from '@/requests'; import { CredentialsTester } from '@/services/credentials-tester.service'; import { OwnershipService } from '@/services/ownership.service'; @@ -598,7 +598,7 @@ export class CredentialsService { // could actually be testing the credential before saving it, so this should cover // the cases we need it for. if ( - !(await userHasScope(user, ['credential:update'], false, { credentialId: credential.id })) + !(await userHasScopes(user, ['credential:update'], false, { credentialId: credential.id })) ) { mergedCredentials.data = decryptedData; } diff --git a/packages/cli/src/decorators/controller.registry.ts b/packages/cli/src/decorators/controller.registry.ts index 41806cb958..3a22090db1 100644 --- a/packages/cli/src/decorators/controller.registry.ts +++ b/packages/cli/src/decorators/controller.registry.ts @@ -11,7 +11,7 @@ import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error'; import type { BooleanLicenseFeature } from '@/interfaces'; import { License } from '@/license'; -import { userHasScope } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions/check-access'; import type { AuthenticatedRequest } from '@/requests'; import { send } from '@/response-helper'; // TODO: move `ResponseHelper.send` to this file @@ -151,7 +151,7 @@ export class ControllerRegistry { const { scope, globalOnly } = accessScope; - if (!(await userHasScope(req.user, [scope], globalOnly, req.params))) { + if (!(await userHasScopes(req.user, [scope], globalOnly, req.params))) { return res.status(403).json({ status: 'error', message: RESPONSE_ERROR_MESSAGES.MISSING_SCOPE, diff --git a/packages/cli/src/permissions/check-access.ts b/packages/cli/src/permissions/check-access.ts index deea44ca99..f4abfcc00f 100644 --- a/packages/cli/src/permissions/check-access.ts +++ b/packages/cli/src/permissions/check-access.ts @@ -10,7 +10,15 @@ import { SharedCredentialsRepository } from '@/databases/repositories/shared-cre import { SharedWorkflowRepository } from '@/databases/repositories/shared-workflow.repository'; import { RoleService } from '@/services/role.service'; -export const userHasScope = async ( +/** + * Check if a user has the required scopes. The check can be: + * + * - only for scopes in the user's global role, or + * - for scopes in the user's global role, else for scopes in the resource roles + * of projects including the user and the resource, else for scopes in the + * project roles in those projects. + */ +export async function userHasScopes( user: User, scopes: Scope[], globalOnly: boolean, @@ -18,15 +26,14 @@ export const userHasScope = async ( credentialId, workflowId, projectId, - }: { credentialId?: string; workflowId?: string; projectId?: string }, -): Promise => { - // Short circuit here since a global role will always have access - if (user.hasGlobalScope(scopes, { mode: 'allOf' })) { - return true; - } else if (globalOnly) { - // The above check already failed so the user doesn't have access - return false; - } + }: { credentialId?: string; workflowId?: string; projectId?: string } /* only one */, +): Promise { + if (user.hasGlobalScope(scopes, { mode: 'allOf' })) return true; + + if (globalOnly) return false; + + // Find which project roles are defined to contain the required scopes. + // Then find projects having this user and having those project roles. const roleService = Container.get(RoleService); const projectRoles = roleService.rolesWithScope('project', scopes); @@ -42,47 +49,29 @@ export const userHasScope = async ( }) ).map((p) => p.id); + // Find which resource roles are defined to contain the required scopes. + // Then find at least one of the above qualifying projects having one of + // those resource roles over the resource being checked. + if (credentialId) { - const exists = await Container.get(SharedCredentialsRepository).find({ - where: { - projectId: In(userProjectIds), - credentialsId: credentialId, - role: In(roleService.rolesWithScope('credential', scopes)), - }, + return await Container.get(SharedCredentialsRepository).existsBy({ + credentialsId: credentialId, + projectId: In(userProjectIds), + role: In(roleService.rolesWithScope('credential', scopes)), }); - - if (!exists.length) { - return false; - } - - return true; } if (workflowId) { - const exists = await Container.get(SharedWorkflowRepository).find({ - where: { - projectId: In(userProjectIds), - workflowId, - role: In(roleService.rolesWithScope('workflow', scopes)), - }, + return await Container.get(SharedWorkflowRepository).existsBy({ + workflowId, + projectId: In(userProjectIds), + role: In(roleService.rolesWithScope('workflow', scopes)), }); - - if (!exists.length) { - return false; - } - - return true; } - if (projectId) { - if (!userProjectIds.includes(projectId)) { - return false; - } - - return true; - } + if (projectId) return userProjectIds.includes(projectId); throw new ApplicationError( - "@ProjectScope decorator was used but does not have a credentialId, workflowId, or projectId in it's URL parameters. This is likely an implementation error. If you're a developer, please check you're URL is correct or that this should be using @GlobalScope.", + "`@ProjectScope` decorator was used but does not have a `credentialId`, `workflowId`, or `projectId` in its URL parameters. This is likely an implementation error. If you're a developer, please check your URL is correct or that this should be using `@GlobalScope`.", ); -}; +} diff --git a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts index 8a49a48093..ed68d4761c 100644 --- a/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts +++ b/packages/cli/src/public-api/v1/shared/middlewares/global.middleware.ts @@ -6,7 +6,7 @@ import { Container } from 'typedi'; import { FeatureNotLicensedError } from '@/errors/feature-not-licensed.error'; import type { BooleanLicenseFeature } from '@/interfaces'; import { License } from '@/license'; -import { userHasScope } from '@/permissions/check-access'; +import { userHasScopes } from '@/permissions/check-access'; import type { AuthenticatedRequest } from '@/requests'; import type { PaginatedRequest } from '../../../types'; @@ -34,7 +34,7 @@ const buildScopeMiddleware = ( params.credentialId = req.params.id; } } - if (!(await userHasScope(req.user, scopes, globalOnly, params))) { + if (!(await userHasScopes(req.user, scopes, globalOnly, params))) { return res.status(403).json({ message: 'Forbidden' }); } From 8db88178511749b19a5878816ef062092fd9f2be Mon Sep 17 00:00:00 2001 From: Csaba Tuncsik Date: Tue, 24 Sep 2024 11:40:28 +0200 Subject: [PATCH 02/29] fix(editor): Fix source control push modal checkboxes (#10910) --- .../SourceControlPushModal.ee.test.ts | 150 ++++++++++++++++++ .../components/SourceControlPushModal.ee.vue | 147 +++++++++-------- 2 files changed, 227 insertions(+), 70 deletions(-) create mode 100644 packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts diff --git a/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts b/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts new file mode 100644 index 0000000000..c7691340fd --- /dev/null +++ b/packages/editor-ui/src/components/SourceControlPushModal.ee.test.ts @@ -0,0 +1,150 @@ +import { within } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import { useRoute } from 'vue-router'; +import { createComponentRenderer } from '@/__tests__/render'; +import SourceControlPushModal from '@/components/SourceControlPushModal.ee.vue'; +import { createTestingPinia } from '@pinia/testing'; +import { createEventBus } from 'n8n-design-system'; +import type { SourceControlAggregatedFile } from '@/Interface'; + +const eventBus = createEventBus(); + +vi.mock('vue-router', () => ({ + useRoute: vi.fn().mockReturnValue({ + params: vi.fn(), + fullPath: vi.fn(), + }), + RouterLink: vi.fn(), +})); + +let route: ReturnType; + +const renderModal = createComponentRenderer(SourceControlPushModal, { + global: { + stubs: { + Modal: { + template: ` +
+ + + + +
+ `, + }, + }, + }, +}); + +describe('SourceControlPushModal', () => { + beforeEach(() => { + route = useRoute(); + }); + + it('mounts', () => { + vi.spyOn(route, 'fullPath', 'get').mockReturnValue(''); + + const { getByTitle } = renderModal({ + pinia: createTestingPinia(), + props: { + data: { + eventBus, + status: [], + }, + }, + }); + expect(getByTitle('Commit and push changes')).toBeInTheDocument(); + }); + + it('should toggle checkboxes', async () => { + const status: SourceControlAggregatedFile[] = [ + { + id: 'gTbbBkkYTnNyX1jD', + name: 'My workflow 1', + type: 'workflow', + status: 'created', + location: 'local', + conflict: false, + file: '/home/user/.n8n/git/workflows/gTbbBkkYTnNyX1jD.json', + updatedAt: '2024-09-20T10:31:40.000Z', + }, + { + id: 'JIGKevgZagmJAnM6', + name: 'My workflow 2', + type: 'workflow', + status: 'created', + location: 'local', + conflict: false, + file: '/home/user/.n8n/git/workflows/JIGKevgZagmJAnM6.json', + updatedAt: '2024-09-20T14:42:51.968Z', + }, + ]; + + vi.spyOn(route, 'fullPath', 'get').mockReturnValue('/home/workflows'); + + const { getByTestId, getAllByTestId } = renderModal({ + pinia: createTestingPinia(), + props: { + data: { + eventBus, + status, + }, + }, + }); + + const files = getAllByTestId('source-control-push-modal-file-checkbox'); + expect(files).toHaveLength(2); + + await userEvent.click(files[0]); + expect(within(files[0]).getByRole('checkbox')).toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + + await userEvent.click(within(files[0]).getByRole('checkbox')); + expect(within(files[0]).getByRole('checkbox')).not.toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + + await userEvent.click(within(files[1]).getByRole('checkbox')); + expect(within(files[0]).getByRole('checkbox')).not.toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).toBeChecked(); + + await userEvent.click(files[1]); + expect(within(files[0]).getByRole('checkbox')).not.toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + + await userEvent.click(within(files[0]).getByText('My workflow 2')); + expect(within(files[0]).getByRole('checkbox')).toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + + await userEvent.click(within(files[1]).getByText('My workflow 1')); + expect(within(files[0]).getByRole('checkbox')).toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).toBeChecked(); + + await userEvent.click(within(files[1]).getByText('My workflow 1')); + expect(within(files[0]).getByRole('checkbox')).toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + + await userEvent.click(getByTestId('source-control-push-modal-toggle-all')); + expect(within(files[0]).getByRole('checkbox')).toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).toBeChecked(); + + await userEvent.click(within(files[0]).getByText('My workflow 2')); + await userEvent.click(within(files[1]).getByText('My workflow 1')); + expect(within(files[0]).getByRole('checkbox')).not.toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + expect( + within(getByTestId('source-control-push-modal-toggle-all')).getByRole('checkbox'), + ).not.toBeChecked(); + + await userEvent.click(within(files[0]).getByText('My workflow 2')); + await userEvent.click(within(files[1]).getByText('My workflow 1')); + expect(within(files[0]).getByRole('checkbox')).toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).toBeChecked(); + expect( + within(getByTestId('source-control-push-modal-toggle-all')).getByRole('checkbox'), + ).toBeChecked(); + + await userEvent.click(getByTestId('source-control-push-modal-toggle-all')); + expect(within(files[0]).getByRole('checkbox')).not.toBeChecked(); + expect(within(files[1]).getByRole('checkbox')).not.toBeChecked(); + }); +}); diff --git a/packages/editor-ui/src/components/SourceControlPushModal.ee.vue b/packages/editor-ui/src/components/SourceControlPushModal.ee.vue index 2f15af6813..0a0d99389a 100644 --- a/packages/editor-ui/src/components/SourceControlPushModal.ee.vue +++ b/packages/editor-ui/src/components/SourceControlPushModal.ee.vue @@ -262,66 +262,66 @@ function getStatusText(file: SourceControlAggregatedFile): string {
- + {{ i18n.baseText('settings.sourceControl.modals.push.description') }} {{ i18n.baseText('settings.sourceControl.modals.push.description.learnMore') }} -
- - - {{ i18n.baseText('settings.sourceControl.modals.push.workflowsToCommit') }} - - - ({{ stagedWorkflowFiles.length }}/{{ workflowFiles.length }}) - - -
- -
- -
- - Deleted Workflow: - Deleted Credential: - {{ file.name || file.id }} - - {{ file.name }} -
- - {{ renderUpdatedAt(file) }} - -
-
-
- - Current workflow - - - {{ getStatusText(file) }} - -
-
-
+ + {{ i18n.baseText('settings.sourceControl.modals.push.workflowsToCommit') }} + + + ({{ stagedWorkflowFiles.length }}/{{ workflowFiles.length }}) + + + + + + + Deleted Workflow: + Deleted Credential: + {{ file.name || file.id }} + + {{ file.name }} + + {{ renderUpdatedAt(file) }} + + + + + Current workflow + + + {{ getStatusText(file) }} + + +
@@ -380,11 +380,15 @@ function getStatusText(file: SourceControlAggregatedFile): string { } .listItem { - margin-top: var(--spacing-2xs); - margin-bottom: var(--spacing-2xs); + display: flex; + width: 100%; + align-items: center; + margin: var(--spacing-2xs) 0 var(--spacing-2xs); + padding: var(--spacing-xs); cursor: pointer; transition: border 0.3s ease; - padding: var(--spacing-xs); + border-radius: var(--border-radius-large); + border: var(--border-base); &:hover { border-color: var(--color-foreground-dark); @@ -397,22 +401,16 @@ function getStatusText(file: SourceControlAggregatedFile): string { &:last-child { margin-bottom: 0; } + + &.hiddenListItem { + display: none !important; + } } -.listItemBody { - display: flex; - flex-direction: row; - align-items: center; -} - -.listItemCheckbox { - display: inline-flex !important; - margin-bottom: 0 !important; - margin-right: var(--spacing-2xs) !important; -} - -.listItemStatus { - margin-left: auto; +.selectAll { + float: left; + clear: both; + margin: 0 0 var(--spacing-2xs); } .footer { @@ -421,3 +419,12 @@ function getStatusText(file: SourceControlAggregatedFile): string { justify-content: flex-end; } + + From 46beda05f6771c31bcf0b6a781976d8261079a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 24 Sep 2024 12:38:11 +0200 Subject: [PATCH 03/29] fix(Notion Node): Allow UUID v8 in notion id checks (#10938) --- .../nodes/Notion/NotionTrigger.node.ts | 17 +++--- .../nodes/Notion/shared/GenericFunctions.ts | 4 +- .../nodes/Notion/shared/constants.ts | 15 ++++++ .../shared/descriptions/BlockDescription.ts | 33 ++++++------ .../Notion/shared/descriptions/Blocks.ts | 17 +++--- .../descriptions/DatabaseDescription.ts | 17 +++--- .../descriptions/DatabasePageDescription.ts | 52 +++++++++---------- .../shared/descriptions/PageDescription.ts | 28 +++++----- .../Notion/test/GenericFunctions.test.ts | 5 +- 9 files changed, 103 insertions(+), 85 deletions(-) create mode 100644 packages/nodes-base/nodes/Notion/shared/constants.ts diff --git a/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts b/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts index e7aa79c431..49079c3c81 100644 --- a/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts +++ b/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts @@ -11,6 +11,12 @@ import moment from 'moment-timezone'; import { notionApiRequest, simplifyObjects } from './shared/GenericFunctions'; import { listSearch } from './shared/methods'; +import { + databaseUrlExtractionRegexp, + databaseUrlValidationRegexp, + idExtractionRegexp, + idValidationRegexp, +} from './shared/constants'; export class NotionTrigger implements INodeType { description: INodeTypeDescription = { @@ -85,16 +91,14 @@ export class NotionTrigger implements INodeType { { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: databaseUrlValidationRegexp, errorMessage: 'Not a valid Notion Database URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: databaseUrlExtractionRegexp, }, }, { @@ -106,15 +110,14 @@ export class NotionTrigger implements INodeType { { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, diff --git a/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts b/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts index 1b6ba6563d..2fd594756f 100644 --- a/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Notion/shared/GenericFunctions.ts @@ -23,6 +23,7 @@ import moment from 'moment-timezone'; import { validate as uuidValidate } from 'uuid'; import set from 'lodash/set'; import { filters } from './descriptions/Filters'; +import { blockUrlExtractionRegexp } from './constants'; function uuidValidateWithoutDashes(this: IExecuteFunctions, value: string) { if (uuidValidate(value)) return true; @@ -1152,8 +1153,7 @@ export function extractBlockId(this: IExecuteFunctions, nodeVersion: number, ite const match = (blockIdRLCData.value as string).match(blockRegex); if (match === null) { - const pageRegex = - /(?:https|http):\/\/www\.notion\.so\/(?:[a-z0-9-]{2,}\/)?(?:[a-zA-Z0-9-]{2,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})/; + const pageRegex = new RegExp(blockUrlExtractionRegexp); const pageMatch = (blockIdRLCData.value as string).match(pageRegex); if (pageMatch === null) { diff --git a/packages/nodes-base/nodes/Notion/shared/constants.ts b/packages/nodes-base/nodes/Notion/shared/constants.ts new file mode 100644 index 0000000000..d2121741e5 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/shared/constants.ts @@ -0,0 +1,15 @@ +const notionIdRegexp = '[0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12}'; + +export const idExtractionRegexp = `^(${notionIdRegexp})`; +export const idValidationRegexp = `${idExtractionRegexp}.*`; + +const baseUrlRegexp = '(?:https|http)://www\\.notion\\.so/(?:[a-z0-9-]{2,}/)?'; + +export const databaseUrlExtractionRegexp = `${baseUrlRegexp}(${notionIdRegexp})`; +export const databaseUrlValidationRegexp = `${databaseUrlExtractionRegexp}.*`; + +export const databasePageUrlExtractionRegexp = `${baseUrlRegexp}(?:[a-zA-Z0-9-]{1,}-)?(${notionIdRegexp})`; +export const databasePageUrlValidationRegexp = `${databasePageUrlExtractionRegexp}.*`; + +export const blockUrlExtractionRegexp = `${baseUrlRegexp}(?:[a-zA-Z0-9-]{2,}-)?(${notionIdRegexp})`; +export const blockUrlValidationRegexp = `${blockUrlExtractionRegexp}.*`; diff --git a/packages/nodes-base/nodes/Notion/shared/descriptions/BlockDescription.ts b/packages/nodes-base/nodes/Notion/shared/descriptions/BlockDescription.ts index fbb48ce26b..a2cab6049f 100644 --- a/packages/nodes-base/nodes/Notion/shared/descriptions/BlockDescription.ts +++ b/packages/nodes-base/nodes/Notion/shared/descriptions/BlockDescription.ts @@ -1,6 +1,12 @@ import type { INodeProperties } from 'n8n-workflow'; import { blocks } from './Blocks'; +import { + blockUrlExtractionRegexp, + blockUrlValidationRegexp, + idExtractionRegexp, + idValidationRegexp, +} from '../constants'; //RLC with fixed regex for blockId const blockIdRLC: INodeProperties = { @@ -20,15 +26,14 @@ const blockIdRLC: INodeProperties = { { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{2,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: blockUrlValidationRegexp, errorMessage: 'Not a valid Notion Block URL', }, }, ], // extractValue: { // type: 'regex', - // regex: 'https:\\/\\/www\\.notion\\.so\\/.+\\?pvs=[0-9]+#([a-f0-9]{2,})', + // regex: blockUrlExtractionRegexp, // }, }, { @@ -101,16 +106,14 @@ export const blockFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{2,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: blockUrlValidationRegexp, errorMessage: 'Not a valid Notion Block URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{2,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: blockUrlExtractionRegexp, }, }, { @@ -122,15 +125,14 @@ export const blockFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Block ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, @@ -176,16 +178,14 @@ export const blockFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{2,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: blockUrlValidationRegexp, errorMessage: 'Not a valid Notion Block URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{2,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: blockUrlExtractionRegexp, }, }, { @@ -197,15 +197,14 @@ export const blockFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Block ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, diff --git a/packages/nodes-base/nodes/Notion/shared/descriptions/Blocks.ts b/packages/nodes-base/nodes/Notion/shared/descriptions/Blocks.ts index 3efaef484e..3b49e23fbe 100644 --- a/packages/nodes-base/nodes/Notion/shared/descriptions/Blocks.ts +++ b/packages/nodes-base/nodes/Notion/shared/descriptions/Blocks.ts @@ -1,4 +1,10 @@ import type { IDisplayOptions, INodeProperties } from 'n8n-workflow'; +import { + databaseUrlExtractionRegexp, + databaseUrlValidationRegexp, + idExtractionRegexp, + idValidationRegexp, +} from '../constants'; const colors = [ { @@ -221,16 +227,14 @@ const typeMention: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: databaseUrlValidationRegexp, errorMessage: 'Not a valid Notion Database URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: databaseUrlExtractionRegexp, }, }, { @@ -242,15 +246,14 @@ const typeMention: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, diff --git a/packages/nodes-base/nodes/Notion/shared/descriptions/DatabaseDescription.ts b/packages/nodes-base/nodes/Notion/shared/descriptions/DatabaseDescription.ts index aee9c8d886..f814139603 100644 --- a/packages/nodes-base/nodes/Notion/shared/descriptions/DatabaseDescription.ts +++ b/packages/nodes-base/nodes/Notion/shared/descriptions/DatabaseDescription.ts @@ -1,4 +1,10 @@ import type { INodeProperties } from 'n8n-workflow'; +import { + databaseUrlExtractionRegexp, + databaseUrlValidationRegexp, + idExtractionRegexp, + idValidationRegexp, +} from '../constants'; export const databaseOperations: INodeProperties[] = [ { @@ -97,8 +103,7 @@ export const databaseFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: databaseUrlValidationRegexp, errorMessage: 'Not a valid Notion Database URL. Hint: use the URL of the database itself, not a page containing it.', }, @@ -106,8 +111,7 @@ export const databaseFields: INodeProperties[] = [ ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: databaseUrlExtractionRegexp, }, }, { @@ -119,15 +123,14 @@ export const databaseFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, diff --git a/packages/nodes-base/nodes/Notion/shared/descriptions/DatabasePageDescription.ts b/packages/nodes-base/nodes/Notion/shared/descriptions/DatabasePageDescription.ts index c1f4a3749f..1a590457da 100644 --- a/packages/nodes-base/nodes/Notion/shared/descriptions/DatabasePageDescription.ts +++ b/packages/nodes-base/nodes/Notion/shared/descriptions/DatabasePageDescription.ts @@ -5,6 +5,14 @@ import { getConditions, getSearchFilters } from '../GenericFunctions'; import { blocks, text } from './Blocks'; import { filters } from './Filters'; +import { + databaseUrlExtractionRegexp, + databaseUrlValidationRegexp, + databasePageUrlExtractionRegexp, + databasePageUrlValidationRegexp, + idExtractionRegexp, + idValidationRegexp, +} from '../constants'; export const databasePageOperations: INodeProperties[] = [ { @@ -114,16 +122,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: databaseUrlValidationRegexp, errorMessage: 'Not a valid Notion Database URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: databaseUrlExtractionRegexp, }, }, { @@ -135,15 +141,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, @@ -600,16 +605,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12}).*', + regex: databasePageUrlValidationRegexp, errorMessage: 'Not a valid Notion Database Page URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})', + regex: databasePageUrlExtractionRegexp, }, }, { @@ -621,15 +624,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database Page ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, @@ -1069,16 +1071,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12}).*', + regex: databasePageUrlValidationRegexp, errorMessage: 'Not a valid Notion Database Page URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})', + regex: databasePageUrlExtractionRegexp, }, }, { @@ -1090,15 +1090,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database Page ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, @@ -1160,16 +1159,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12}).*', + regex: databasePageUrlValidationRegexp, errorMessage: 'Not a valid Notion Database Page URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})', + regex: databasePageUrlExtractionRegexp, }, }, { @@ -1181,15 +1178,14 @@ export const databasePageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Database Page ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{4}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, diff --git a/packages/nodes-base/nodes/Notion/shared/descriptions/PageDescription.ts b/packages/nodes-base/nodes/Notion/shared/descriptions/PageDescription.ts index 93c79fb2cf..37d57799cb 100644 --- a/packages/nodes-base/nodes/Notion/shared/descriptions/PageDescription.ts +++ b/packages/nodes-base/nodes/Notion/shared/descriptions/PageDescription.ts @@ -1,6 +1,12 @@ import type { INodeProperties } from 'n8n-workflow'; import { blocks } from './Blocks'; +import { + databasePageUrlExtractionRegexp, + databasePageUrlValidationRegexp, + idExtractionRegexp, + idValidationRegexp, +} from '../constants'; export const pageOperations: INodeProperties[] = [ { @@ -93,16 +99,14 @@ export const pageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: databasePageUrlValidationRegexp, errorMessage: 'Not a valid Notion Database Page URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: databasePageUrlExtractionRegexp, }, }, { @@ -114,15 +118,14 @@ export const pageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Page ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, @@ -173,16 +176,14 @@ export const pageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*', + regex: databasePageUrlValidationRegexp, errorMessage: 'Not a valid Notion Database Page URL', }, }, ], extractValue: { type: 'regex', - regex: - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})', + regex: databasePageUrlExtractionRegexp, }, }, { @@ -194,15 +195,14 @@ export const pageFields: INodeProperties[] = [ { type: 'regex', properties: { - regex: - '^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*', + regex: idValidationRegexp, errorMessage: 'Not a valid Notion Page ID', }, }, ], extractValue: { type: 'regex', - regex: '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})', + regex: idExtractionRegexp, }, url: '=https://www.notion.so/{{$value.replace(/-/g, "")}}', }, diff --git a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts index 543292abe8..40b01ea18b 100644 --- a/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts +++ b/packages/nodes-base/nodes/Notion/test/GenericFunctions.test.ts @@ -1,3 +1,4 @@ +import { databasePageUrlExtractionRegexp } from '../shared/constants'; import { extractPageId, formatBlocks } from '../shared/GenericFunctions'; describe('Test NotionV2, formatBlocks', () => { @@ -41,11 +42,9 @@ describe('Test Notion', () => { 'f4c1217e48f711ef94540242ac120002', // Random v1 UUID ]; describe('extractPageId From URL', () => { - const extractPattern = - '(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?(?:[a-zA-Z0-9-]{1,}-)?([0-9a-f]{8}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{4}[0-9a-f]{12})'; // RLC does some Regex extraction before extractPageId is called const extractIdFromUrl = (url: string): string => { - const match = url.match(extractPattern); + const match = url.match(databasePageUrlExtractionRegexp); return match ? match[1] : url; }; From dcc1c72fc4b56c3252183541b22da801804d4f79 Mon Sep 17 00:00:00 2001 From: Eugene Date: Tue, 24 Sep 2024 16:48:58 +0200 Subject: [PATCH 04/29] feat(editor): Show a notice before deleting annotated executions (#10934) --- .../global/GlobalExecutionsList.vue | 46 ++++++++++++++++++- .../workflow/WorkflowExecutionsPreview.vue | 16 ++++++- .../src/plugins/i18n/locales/en.json | 3 ++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue b/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue index a1bcc95f94..f526867d51 100644 --- a/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue +++ b/packages/editor-ui/src/components/executions/global/GlobalExecutionsList.vue @@ -2,7 +2,11 @@ import { watch, computed, ref, onMounted } from 'vue'; import ExecutionsFilter from '@/components/executions/ExecutionsFilter.vue'; import GlobalExecutionsListItem from '@/components/executions/global/GlobalExecutionsListItem.vue'; -import { MODAL_CONFIRM } from '@/constants'; +import { + EnterpriseEditionFeature, + EXECUTION_ANNOTATION_EXPERIMENT, + MODAL_CONFIRM, +} from '@/constants'; import { useToast } from '@/composables/useToast'; import { useMessage } from '@/composables/useMessage'; import { useI18n } from '@/composables/useI18n'; @@ -13,6 +17,8 @@ import { useWorkflowsStore } from '@/stores/workflows.store'; import { useExecutionsStore } from '@/stores/executions.store'; import type { PermissionsRecord } from '@/permissions'; import { getResourcePermissions } from '@/permissions'; +import { usePostHog } from '@/stores/posthog.store'; +import { useSettingsStore } from '@/stores/settings.store'; const props = withDefaults( defineProps<{ @@ -36,6 +42,8 @@ const i18n = useI18n(); const telemetry = useTelemetry(); const workflowsStore = useWorkflowsStore(); const executionsStore = useExecutionsStore(); +const posthogStore = usePostHog(); +const settingsStore = useSettingsStore(); const isMounted = ref(false); const allVisibleSelected = ref(false); @@ -63,6 +71,12 @@ const workflows = computed(() => { ]; }); +const isAnnotationEnabled = computed( + () => + settingsStore.isEnterpriseFeatureEnabled[EnterpriseEditionFeature.AdvancedExecutionFilters] && + posthogStore.isFeatureEnabled(EXECUTION_ANNOTATION_EXPERIMENT), +); + watch( () => props.executions, () => { @@ -109,10 +123,18 @@ function toggleSelectExecution(execution: ExecutionSummary) { } async function handleDeleteSelected() { - const deleteExecutions = await message.confirm( + // Prepend the message with a note about annotations if the feature is enabled + const confirmationText = [ + isAnnotationEnabled.value && i18n.baseText('executionsList.confirmMessage.annotationsNote'), i18n.baseText('executionsList.confirmMessage.message', { interpolate: { count: selectedCount.value.toString() }, }), + ] + .filter(Boolean) + .join(' '); + + const deleteExecutions = await message.confirm( + confirmationText, i18n.baseText('executionsList.confirmMessage.headline'), { type: 'warning', @@ -258,6 +280,26 @@ async function stopExecution(execution: ExecutionSummary) { } async function deleteExecution(execution: ExecutionSummary) { + const hasAnnotation = + !!execution.annotation && (execution.annotation.vote || execution.annotation.tags.length > 0); + + // Show a confirmation dialog if the execution has an annotation + if (hasAnnotation) { + const deleteConfirmed = await message.confirm( + i18n.baseText('executionsList.confirmMessage.annotatedExecutionMessage'), + i18n.baseText('executionDetails.confirmMessage.headline'), + { + type: 'warning', + confirmButtonText: i18n.baseText('executionDetails.confirmMessage.confirmButtonText'), + cancelButtonText: '', + }, + ); + + if (deleteConfirmed !== MODAL_CONFIRM) { + return; + } + } + try { await executionsStore.deleteExecutions({ ids: [execution.id] }); diff --git a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.vue b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.vue index 081c8a0d9f..9f06b080f0 100644 --- a/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.vue +++ b/packages/editor-ui/src/components/executions/workflow/WorkflowExecutionsPreview.vue @@ -72,9 +72,23 @@ const isAnnotationEnabled = computed( posthogStore.isFeatureEnabled(EXECUTION_ANNOTATION_EXPERIMENT), ); +const hasAnnotation = computed( + () => + !!props.execution?.annotation && + (props.execution?.annotation.vote || props.execution?.annotation.tags.length > 0), +); + async function onDeleteExecution(): Promise { - const deleteConfirmed = await message.confirm( + // Prepend the message with a note about annotations if they exist + const confirmationText = [ + hasAnnotation.value && locale.baseText('executionDetails.confirmMessage.annotationsNote'), locale.baseText('executionDetails.confirmMessage.message'), + ] + .filter(Boolean) + .join(' '); + + const deleteConfirmed = await message.confirm( + confirmationText, locale.baseText('executionDetails.confirmMessage.headline'), { type: 'warning', diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 6159437d8a..d007b41da1 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -649,6 +649,7 @@ "executionDetails.confirmMessage.confirmButtonText": "Yes, delete", "executionDetails.confirmMessage.headline": "Delete Execution?", "executionDetails.confirmMessage.message": "Are you sure that you want to delete the current execution?", + "executionDetails.confirmMessage.annotationsNote": "By deleting this you will also remove the associated annotation data.", "executionDetails.deleteExecution": "Delete this execution", "executionDetails.executionFailed": "Execution failed", "executionDetails.executionFailed.recoveredNodeTitle": "Can’t show data", @@ -689,6 +690,8 @@ "executionsList.confirmMessage.confirmButtonText": "Yes, delete", "executionsList.confirmMessage.headline": "Delete Executions?", "executionsList.confirmMessage.message": "Are you sure that you want to delete the {count} selected execution(s)?", + "executionsList.confirmMessage.annotationsNote": "By deleting these executions you will also remove the associated annotation data.", + "executionsList.confirmMessage.annotatedExecutionMessage": "By deleting this you will also remove the associated annotation data. Are you sure that you want to delete the selected execution?", "executionsList.clearSelection": "Clear selection", "executionsList.error": "Error", "executionsList.filters": "Filters", From a81256aff5e70b57cd47e871c3389f749577e6d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 24 Sep 2024 16:53:18 +0200 Subject: [PATCH 05/29] fix(core): Fix sentry de-duplication by migrating from event-processors to beforeSend (no-changelog) (#10947) --- packages/cli/src/error-reporting.ts | 50 ++++++++++++++--------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/packages/cli/src/error-reporting.ts b/packages/cli/src/error-reporting.ts index 64a7696a77..5628615faa 100644 --- a/packages/cli/src/error-reporting.ts +++ b/packages/cli/src/error-reporting.ts @@ -29,7 +29,7 @@ export const initErrorHandling = async () => { DEPLOYMENT_NAME: serverName, } = process.env; - const { init, captureException, addEventProcessor } = await import('@sentry/node'); + const { init, captureException } = await import('@sentry/node'); const { RewriteFrames } = await import('@sentry/integrations'); const { Integrations } = await import('@sentry/node'); @@ -41,6 +41,8 @@ export const initErrorHandling = async () => { 'OnUnhandledRejection', 'ContextLines', ]; + const seenErrors = new Set(); + init({ dsn, release, @@ -62,34 +64,32 @@ export const initErrorHandling = async () => { }, }), ], - }); + beforeSend(event, { originalException }) { + if (!originalException) return null; - const seenErrors = new Set(); - addEventProcessor((event, { originalException }) => { - if (!originalException) return null; + if ( + originalException instanceof QueryFailedError && + ['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => originalException.message.includes(errMsg)) + ) { + return null; + } - if ( - originalException instanceof QueryFailedError && - ['SQLITE_FULL', 'SQLITE_IOERR'].some((errMsg) => originalException.message.includes(errMsg)) - ) { - return null; - } + if (originalException instanceof ApplicationError) { + const { level, extra, tags } = originalException; + if (level === 'warning') return null; + event.level = level; + if (extra) event.extra = { ...event.extra, ...extra }; + if (tags) event.tags = { ...event.tags, ...tags }; + } - if (originalException instanceof ApplicationError) { - const { level, extra, tags } = originalException; - if (level === 'warning') return null; - event.level = level; - if (extra) event.extra = { ...event.extra, ...extra }; - if (tags) event.tags = { ...event.tags, ...tags }; - } + if (originalException instanceof Error && originalException.stack) { + const eventHash = createHash('sha1').update(originalException.stack).digest('base64'); + if (seenErrors.has(eventHash)) return null; + seenErrors.add(eventHash); + } - if (originalException instanceof Error && originalException.stack) { - const eventHash = createHash('sha1').update(originalException.stack).digest('base64'); - if (seenErrors.has(eventHash)) return null; - seenErrors.add(eventHash); - } - - return event; + return event; + }, }); ErrorReporterProxy.init({ From ad60d49b4251138a7c69cb5e9f00c3ef875486e0 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 24 Sep 2024 17:14:39 +0200 Subject: [PATCH 06/29] fix(editor): Use correct output for connected nodes in schema view (#10928) --- .../src/components/RunDataSchema.vue | 29 +- .../__tests__/RunDataSchema.test.ts | 68 +- .../__snapshots__/RunDataSchema.test.ts.snap | 1491 +++++++++++++++++ 3 files changed, 1568 insertions(+), 20 deletions(-) diff --git a/packages/editor-ui/src/components/RunDataSchema.vue b/packages/editor-ui/src/components/RunDataSchema.vue index a76012553f..50e5afd31f 100644 --- a/packages/editor-ui/src/components/RunDataSchema.vue +++ b/packages/editor-ui/src/components/RunDataSchema.vue @@ -42,6 +42,7 @@ type SchemaNode = { depth: number; loading: boolean; open: boolean; + connectedOutputIndexes: number[]; itemsCount: number | null; schema: Schema | null; }; @@ -94,6 +95,7 @@ const nodes = computed(() => { return { node: fullNode, + connectedOutputIndexes: node.indicies, depth: node.depth, itemsCount, nodeType, @@ -141,19 +143,17 @@ const highlight = computed(() => ndvStore.highlightDraggables); const allNodesOpen = computed(() => nodes.value.every((node) => node.open)); const noNodesOpen = computed(() => nodes.value.every((node) => !node.open)); -const loadNodeData = async (node: INodeUi) => { +const loadNodeData = async ({ node, connectedOutputIndexes }: SchemaNode) => { const pinData = workflowsStore.pinDataByNodeName(node.name); const data = pinData ?? - executionDataToJson( - getNodeInputData( - node, - props.runIndex, - props.outputIndex, - props.paneType, - props.connectionType, - ) ?? [], - ); + connectedOutputIndexes + .map((outputIndex) => + executionDataToJson( + getNodeInputData(node, props.runIndex, outputIndex, props.paneType, props.connectionType), + ), + ) + .flat(); nodesData.value[node.name] = { schema: getSchemaForExecutionData(data), @@ -161,7 +161,8 @@ const loadNodeData = async (node: INodeUi) => { }; }; -const toggleOpenNode = async ({ node, schema, open }: SchemaNode, exclusive = false) => { +const toggleOpenNode = async (schemaNode: SchemaNode, exclusive = false) => { + const { node, schema, open } = schemaNode; disableScrollInView.value = false; if (open) { nodesOpen.value[node.name] = false; @@ -170,7 +171,7 @@ const toggleOpenNode = async ({ node, schema, open }: SchemaNode, exclusive = fa if (!schema) { nodesLoading.value[node.name] = true; - await loadNodeData(node); + await loadNodeData(schemaNode); nodesLoading.value[node.name] = false; } @@ -182,8 +183,8 @@ const toggleOpenNode = async ({ node, schema, open }: SchemaNode, exclusive = fa }; const openAllNodes = async () => { - const nodesToLoad = nodes.value.filter((node) => !node.schema).map(({ node }) => node); - await Promise.all(nodesToLoad.map(async (node) => await loadNodeData(node))); + const nodesToLoad = nodes.value.filter((node) => !node.schema); + await Promise.all(nodesToLoad.map(loadNodeData)); nodesOpen.value = Object.fromEntries(nodes.value.map(({ node }) => [node.name, true])); }; diff --git a/packages/editor-ui/src/components/__tests__/RunDataSchema.test.ts b/packages/editor-ui/src/components/__tests__/RunDataSchema.test.ts index 8ac02d2152..5c8aedb36f 100644 --- a/packages/editor-ui/src/components/__tests__/RunDataSchema.test.ts +++ b/packages/editor-ui/src/components/__tests__/RunDataSchema.test.ts @@ -2,13 +2,19 @@ import { createComponentRenderer } from '@/__tests__/render'; import RunDataJsonSchema from '@/components/RunDataSchema.vue'; import { useWorkflowsStore } from '@/stores/workflows.store'; import { userEvent } from '@testing-library/user-event'; -import { cleanup, within } from '@testing-library/vue'; +import { cleanup, within, waitFor } from '@testing-library/vue'; import { createPinia, setActivePinia } from 'pinia'; -import { createTestNode, defaultNodeDescriptions } from '@/__tests__/mocks'; -import { SET_NODE_TYPE } from '@/constants'; +import { + createTestNode, + defaultNodeDescriptions, + mockNodeTypeDescription, +} from '@/__tests__/mocks'; +import { IF_NODE_TYPE, SET_NODE_TYPE } from '@/constants'; import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { mock } from 'vitest-mock-extended'; import type { IWorkflowDb } from '@/Interface'; +import { NodeConnectionType, type IDataObject } from 'n8n-workflow'; +import * as nodeHelpers from '@/composables/useNodeHelpers'; const mockNode1 = createTestNode({ name: 'Set1', @@ -31,13 +37,20 @@ const disabledNode = createTestNode({ disabled: true, }); +const ifNode = createTestNode({ + name: 'If', + type: IF_NODE_TYPE, + typeVersion: 1, + disabled: false, +}); + async function setupStore() { const workflow = mock({ id: '123', name: 'Test Workflow', connections: {}, active: true, - nodes: [mockNode1, mockNode2, disabledNode], + nodes: [mockNode1, mockNode2, disabledNode, ifNode], }); const pinia = createPinia(); @@ -46,12 +59,33 @@ async function setupStore() { const workflowsStore = useWorkflowsStore(); const nodeTypesStore = useNodeTypesStore(); - nodeTypesStore.setNodeTypes(defaultNodeDescriptions); + nodeTypesStore.setNodeTypes([ + ...defaultNodeDescriptions, + mockNodeTypeDescription({ + name: IF_NODE_TYPE, + outputs: [NodeConnectionType.Main, NodeConnectionType.Main], + }), + ]); workflowsStore.workflow = workflow; return pinia; } +function mockNodeOutputData(nodeName: string, data: IDataObject[], outputIndex = 0) { + const originalNodeHelpers = nodeHelpers.useNodeHelpers(); + vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => { + return { + ...originalNodeHelpers, + getNodeInputData: vi.fn((node, _, output) => { + if (node.name === nodeName && output === outputIndex) { + return data.map((json) => ({ json })); + } + return []; + }), + }; + }); +} + describe('RunDataSchema.vue', () => { let renderComponent: ReturnType; @@ -122,7 +156,7 @@ describe('RunDataSchema.vue', () => { expect(within(nodes[1]).getByTestId('run-data-schema-node-schema')).toMatchSnapshot(); }); - it('renders schema for in output pane', async () => { + it('renders schema in output pane', async () => { const { container } = renderComponent({ props: { nodes: [], @@ -183,6 +217,28 @@ describe('RunDataSchema.vue', () => { ); }); + it('renders schema for correct output branch', async () => { + mockNodeOutputData( + 'If', + [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + ], + 1, + ); + const { getByTestId } = renderComponent({ + props: { + nodes: [{ name: 'If', indicies: [1], depth: 2 }], + }, + }); + + await waitFor(() => { + expect(getByTestId('run-data-schema-node-name')).toHaveTextContent('If'); + expect(getByTestId('run-data-schema-node-item-count')).toHaveTextContent('2 items'); + expect(getByTestId('run-data-schema-node-schema')).toMatchSnapshot(); + }); + }); + test.each([[[{ tx: false }, { tx: false }]], [[{ tx: '' }, { tx: '' }]], [[{ tx: [] }]]])( 'renders schema instead of showing no data for %o', (data) => { diff --git a/packages/editor-ui/src/components/__tests__/__snapshots__/RunDataSchema.test.ts.snap b/packages/editor-ui/src/components/__tests__/__snapshots__/RunDataSchema.test.ts.snap index 942eb55aa3..de6c683589 100644 --- a/packages/editor-ui/src/components/__tests__/__snapshots__/RunDataSchema.test.ts.snap +++ b/packages/editor-ui/src/components/__tests__/__snapshots__/RunDataSchema.test.ts.snap @@ -1,5 +1,1059 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`RunDataSchema.vue > renders schema for correct output branch 1`] = ` +
+
+
+
+ + +
+ + +
+
+ +
+
+
+ + + + + + + + name + + + + +
+ + + + + + + + + John + + + + + + +
+ + + +
+
+
+
+ + + + + + + + age + + + + +
+ + + + + + + + + 22 + + + + + + +
+ + + +
+
+
+
+ + + + + + + + hobbies + + + + +
+ +
+ + +
+
+ +
+
+
+ + + + + + + hobbies + + + + + + + + [0] + + + + +
+ + + + + + + + + surfing + + + + + + +
+ + + +
+
+
+
+ + + + + + + hobbies + + + + + + + + [1] + + + + +
+ + + + + + + + + traveling + + + + + + +
+ + + +
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`RunDataSchema.vue > renders schema for correct output branch 2`] = ` +
+
+
+
+ + +
+ + +
+
+ +
+
+
+ + + + + + + + name + + + + +
+ + + + + + + + + John + + + + + + +
+ + + +
+
+
+
+ + + + + + + + age + + + + +
+ + + + + + + + + 22 + + + + + + +
+ + + +
+
+
+
+ + + + + + + + hobbies + + + + +
+ +
+ + +
+
+ +
+
+
+ + + + + + + hobbies + + + + + + + + [0] + + + + +
+ + + + + + + + + surfing + + + + + + +
+ + + +
+
+
+
+ + + + + + + hobbies + + + + + + + + [1] + + + + +
+ + + + + + + + + traveling + + + + + + +
+ + + +
+ +
+
+
+ +
+
+
+
+
+`; + +exports[`RunDataSchema.vue > renders schema for correct output branch 3`] = ` +
+
+
+
+ + +
+ + +
+
+ +
+
+
+ + + + + + + + id + + + + +
+ + + + + + + + + 1 + + + + + + +
+ + + +
+
+
+
+ + + + + + + + name + + + + +
+ + + + + + + + + John + + + + + + +
+ + + +
+ +
+
+
+
+
+`; + exports[`RunDataSchema.vue > renders schema for data 1`] = `
renders schema for in output pane 1`] = `
`; +exports[`RunDataSchema.vue > renders schema in output pane 1`] = ` +
+
+
+
+
+ + +
+ + +
+
+ +
+
+
+ + + + + + + + name + + + + +
+ + + + + + + + + John + + + + + + +
+ + + +
+
+
+
+ + + + + + + + age + + + + +
+ + + + + + + + + 22 + + + + + + +
+ + + +
+
+
+
+ + + + + + + + hobbies + + + + +
+ +
+ + +
+
+ +
+
+
+ + + + + + + hobbies + + + + + + + + [0] + + + + +
+ + + + + + + + + surfing + + + + + + +
+ + + +
+
+
+
+ + + + + + + hobbies + + + + + + + + [1] + + + + +
+ + + + + + + + + traveling + + + + + + +
+ + + +
+ +
+
+
+ +
+
+
+
+
+
+`; + exports[`RunDataSchema.vue > renders schema with spaces and dots 1`] = `
Date: Tue, 24 Sep 2024 18:41:07 +0300 Subject: [PATCH 07/29] fix(Google Sheets Node): Insert data if sheet is empty instead of error (#10942) --- .../Google/Sheet/test/v2/node/append.test.ts | 59 +++++++++++ .../Sheet/test/v2/node/appendOrUpdate.test.ts | 98 +++++++++++++++++++ .../v2/actions/sheet/append.operation.ts | 6 +- .../actions/sheet/appendOrUpdate.operation.ts | 14 ++- .../Sheet/v2/actions/versionDescription.ts | 7 ++ 5 files changed, 178 insertions(+), 6 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts create mode 100644 packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts new file mode 100644 index 0000000000..d424f554bd --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/append.test.ts @@ -0,0 +1,59 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import { execute } from '../../../v2/actions/sheet/append.operation'; +import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet'; + +describe('Google Sheet - Append', () => { + let mockExecuteFunctions: MockProxy; + let mockGoogleSheet: MockProxy; + + beforeEach(() => { + mockExecuteFunctions = mock(); + mockGoogleSheet = mock(); + }); + + it('should insert input data if sheet is empty', async () => { + mockExecuteFunctions.getInputData.mockReturnValueOnce([ + { + json: { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + pairedItem: { + item: 0, + input: undefined, + }, + }, + ]); + + mockExecuteFunctions.getNode.mockReturnValueOnce(mock({ typeVersion: 4.5 })); + mockExecuteFunctions.getNodeParameter + .mockReturnValueOnce('USER_ENTERED') // valueInputMode + .mockReturnValueOnce({}); // options + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('defineBelow'); // dataMode + + mockGoogleSheet.getData.mockResolvedValueOnce(undefined); + mockGoogleSheet.getData.mockResolvedValueOnce(undefined); + mockGoogleSheet.updateRows.mockResolvedValueOnce(undefined); + + mockGoogleSheet.updateRows.mockResolvedValueOnce([]); + + mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]); + mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]); + + await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'); + + expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith('Sheet1', [['name', 'text']], 'RAW', 1); + expect(mockGoogleSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('1234', 1, 0); + expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({ + inputData: [{ name: 'NEW NAME', text: 'NEW TEXT' }], + keyRowIndex: 1, + lastRow: 2, + range: 'Sheet1', + valueInputMode: 'USER_ENTERED', + }); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts new file mode 100644 index 0000000000..552567609f --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/appendOrUpdate.test.ts @@ -0,0 +1,98 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import { execute } from '../../../v2/actions/sheet/appendOrUpdate.operation'; +import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet'; + +describe('Google Sheet - Append or Update', () => { + let mockExecuteFunctions: MockProxy; + let mockGoogleSheet: MockProxy; + + beforeEach(() => { + mockExecuteFunctions = mock(); + mockGoogleSheet = mock(); + }); + + it('should insert input data if sheet is empty', async () => { + mockExecuteFunctions.getInputData.mockReturnValueOnce([ + { + json: { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + pairedItem: { + item: 0, + input: undefined, + }, + }, + ]); + + mockExecuteFunctions.getNode.mockReturnValueOnce(mock({ typeVersion: 4.5 })); + mockExecuteFunctions.getNodeParameter + .mockReturnValueOnce('USER_ENTERED') // valueInputMode + .mockReturnValueOnce({}); // options + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('defineBelow'); // dataMode + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce([]); // columns.schema + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(['row_number']); // columnsToMatchOn + mockExecuteFunctions.getNode.mockReturnValueOnce(mock()); + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce([]); // columns.matchingColumns + + mockGoogleSheet.getData.mockResolvedValueOnce(undefined); + + mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]); + mockGoogleSheet.updateRows.mockResolvedValueOnce([]); + + mockGoogleSheet.prepareDataForUpdateOrUpsert.mockResolvedValueOnce({ + updateData: [], + appendData: [ + { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + ], + }); + + mockGoogleSheet.appendEmptyRowsOrColumns.mockResolvedValueOnce([]); + mockGoogleSheet.appendSheetData.mockResolvedValueOnce([]); + + await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1', '1234'); + + expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({ + dataStartRowIndex: 1, + keyIndex: -1, + range: 'Sheet1!A:Z', + sheetData: [['name', 'text']], + valueRenderMode: 'UNFORMATTED_VALUE', + }); + + expect(mockGoogleSheet.updateRows).toHaveBeenCalledWith( + 'Sheet1', + [['name', 'text']], + 'USER_ENTERED', + 1, + ); + expect(mockGoogleSheet.prepareDataForUpdateOrUpsert).toHaveBeenCalledWith({ + columnNamesList: [['name', 'text']], + columnValuesList: [], + dataStartRowIndex: 1, + indexKey: 'row_number', + inputData: [{ name: 'NEW NAME', row_number: 3, text: 'NEW TEXT' }], + keyRowIndex: 0, + range: 'Sheet1!A:Z', + upsert: true, + valueRenderMode: 'UNFORMATTED_VALUE', + }); + expect(mockGoogleSheet.appendEmptyRowsOrColumns).toHaveBeenCalledWith('1234', 1, 0); + expect(mockGoogleSheet.appendSheetData).toHaveBeenCalledWith({ + columnNamesList: [['name', 'text']], + inputData: [{ name: 'NEW NAME', row_number: 3, text: 'NEW TEXT' }], + keyRowIndex: 1, + lastRow: 2, + range: 'Sheet1!A:Z', + valueInputMode: 'USER_ENTERED', + }); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts index 03fb7a75a0..907a70b0fc 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/append.operation.ts @@ -211,7 +211,7 @@ export async function execute( ): Promise { const items = this.getInputData(); const nodeVersion = this.getNode().typeVersion; - const dataMode = + let dataMode = nodeVersion < 4 ? (this.getNodeParameter('dataMode', 0) as string) : (this.getNodeParameter('columns.mappingMode', 0) as string); @@ -228,6 +228,10 @@ export async function execute( const sheetData = await sheet.getData(range, 'FORMATTED_VALUE'); + if (sheetData === undefined || !sheetData.length) { + dataMode = 'autoMapInputData'; + } + if (nodeVersion >= 4.4 && dataMode !== 'autoMapInputData') { //not possible to refresh columns when mode is autoMapInputData if (sheetData?.[keyRowIndex - 1] === undefined) { diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts index caa3392230..f6f3e5cd3d 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts @@ -257,7 +257,7 @@ export async function execute( } } - const dataMode = + let dataMode = nodeVersion < 4 ? (this.getNodeParameter('dataMode', 0) as string) : (this.getNodeParameter('columns.mappingMode', 0) as string); @@ -267,10 +267,14 @@ export async function execute( const sheetData = (await sheet.getData(sheetName, 'FORMATTED_VALUE')) ?? []; if (!sheetData[keyRowIndex] && dataMode !== 'autoMapInputData') { - throw new NodeOperationError( - this.getNode(), - `Could not retrieve the column names from row ${keyRowIndex + 1}`, - ); + if (!sheetData.length) { + dataMode = 'autoMapInputData'; + } else { + throw new NodeOperationError( + this.getNode(), + `Could not retrieve the column names from row ${keyRowIndex + 1}`, + ); + } } columnNames = sheetData[keyRowIndex] ?? []; diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts index e41a8b9a24..5ac6b0a195 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/versionDescription.ts @@ -26,6 +26,13 @@ export const versionDescription: INodeTypeDescription = { whenToDisplay: 'beforeExecution', location: 'outputPane', }, + { + message: 'No columns found in Google Sheet. All rows will be appended', + displayCondition: + '={{ ["appendOrUpdate", "append"].includes($parameter["operation"]) && $parameter?.columns?.mappingMode === "defineBelow" && !$parameter?.columns?.schema?.length }}', + whenToDisplay: 'beforeExecution', + location: 'outputPane', + }, ], credentials: [ { From 6de4dfff87e4da888567081a9928d9682bdea11d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 24 Sep 2024 17:49:22 +0200 Subject: [PATCH 08/29] feat(editor): Setup Sentry integration (#10945) --- package.json | 2 +- .../@n8n/config/src/configs/sentry.config.ts | 12 + packages/@n8n/config/src/index.ts | 4 + packages/@n8n/config/test/config.test.ts | 4 + packages/cli/src/config/schema.ts | 8 - packages/cli/src/error-reporting.ts | 6 +- packages/cli/src/events/events.controller.ts | 16 + packages/cli/src/requests.ts | 3 + packages/cli/src/server.ts | 27 +- packages/cli/src/services/frontend.service.ts | 14 +- .../cli/src/workflows/workflows.controller.ts | 2 +- packages/editor-ui/index.html | 1 + packages/editor-ui/package.json | 3 +- packages/editor-ui/src/api/events.ts | 6 + packages/editor-ui/src/main.ts | 6 + packages/editor-ui/src/shims.d.ts | 2 + packages/editor-ui/vite.config.mts | 6 +- ...2.17.0.patch => @sentry__cli@2.36.2.patch} | 2 +- pnpm-lock.yaml | 304 +++++++++++++----- 19 files changed, 307 insertions(+), 121 deletions(-) create mode 100644 packages/@n8n/config/src/configs/sentry.config.ts create mode 100644 packages/cli/src/events/events.controller.ts create mode 100644 packages/editor-ui/src/api/events.ts rename patches/{@sentry__cli@2.17.0.patch => @sentry__cli@2.36.2.patch} (77%) diff --git a/package.json b/package.json index 216ca673b6..589b515f44 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ }, "patchedDependencies": { "typedi@0.10.0": "patches/typedi@0.10.0.patch", - "@sentry/cli@2.17.0": "patches/@sentry__cli@2.17.0.patch", + "@sentry/cli@2.36.2": "patches/@sentry__cli@2.36.2.patch", "pkce-challenge@3.0.0": "patches/pkce-challenge@3.0.0.patch", "pyodide@0.23.4": "patches/pyodide@0.23.4.patch", "@types/express-serve-static-core@4.17.43": "patches/@types__express-serve-static-core@4.17.43.patch", diff --git a/packages/@n8n/config/src/configs/sentry.config.ts b/packages/@n8n/config/src/configs/sentry.config.ts new file mode 100644 index 0000000000..d1067f9984 --- /dev/null +++ b/packages/@n8n/config/src/configs/sentry.config.ts @@ -0,0 +1,12 @@ +import { Config, Env } from '../decorators'; + +@Config +export class SentryConfig { + /** Sentry DSN for the backend. */ + @Env('N8N_SENTRY_DSN') + backendDsn: string = ''; + + /** Sentry DSN for the frontend . */ + @Env('N8N_FRONTEND_SENTRY_DSN') + frontendDsn: string = ''; +} diff --git a/packages/@n8n/config/src/index.ts b/packages/@n8n/config/src/index.ts index c6e456709c..5098093db4 100644 --- a/packages/@n8n/config/src/index.ts +++ b/packages/@n8n/config/src/index.ts @@ -8,6 +8,7 @@ import { ExternalStorageConfig } from './configs/external-storage.config'; import { NodesConfig } from './configs/nodes.config'; import { PublicApiConfig } from './configs/public-api.config'; import { ScalingModeConfig } from './configs/scaling-mode.config'; +import { SentryConfig } from './configs/sentry.config'; import { TemplatesConfig } from './configs/templates.config'; import { UserManagementConfig } from './configs/user-management.config'; import { VersionNotificationsConfig } from './configs/version-notifications.config'; @@ -49,6 +50,9 @@ export class GlobalConfig { @Nested workflows: WorkflowsConfig; + @Nested + sentry: SentryConfig; + /** Path n8n is deployed to */ @Env('N8N_PATH') path: string = '/'; diff --git a/packages/@n8n/config/test/config.test.ts b/packages/@n8n/config/test/config.test.ts index 1d3e5f971d..11fd97a5db 100644 --- a/packages/@n8n/config/test/config.test.ts +++ b/packages/@n8n/config/test/config.test.ts @@ -221,6 +221,10 @@ describe('GlobalConfig', () => { }, }, }, + sentry: { + backendDsn: '', + frontendDsn: '', + }, }; it('should use all default values when no env variables are defined', () => { diff --git a/packages/cli/src/config/schema.ts b/packages/cli/src/config/schema.ts index 0db300eaf0..e811fe8e10 100644 --- a/packages/cli/src/config/schema.ts +++ b/packages/cli/src/config/schema.ts @@ -452,14 +452,6 @@ export const schema = { env: 'N8N_DIAGNOSTICS_POSTHOG_API_HOST', }, }, - sentry: { - dsn: { - doc: 'Data source name for error tracking on Sentry', - format: String, - default: '', - env: 'N8N_SENTRY_DSN', - }, - }, frontend: { doc: 'Diagnostics config for frontend.', format: String, diff --git a/packages/cli/src/error-reporting.ts b/packages/cli/src/error-reporting.ts index 5628615faa..d1ecd39198 100644 --- a/packages/cli/src/error-reporting.ts +++ b/packages/cli/src/error-reporting.ts @@ -1,9 +1,9 @@ +import { GlobalConfig } from '@n8n/config'; // eslint-disable-next-line n8n-local-rules/misplaced-n8n-typeorm-import import { QueryFailedError } from '@n8n/typeorm'; import { createHash } from 'crypto'; import { ErrorReporterProxy, ApplicationError } from 'n8n-workflow'; - -import config from '@/config'; +import Container from 'typedi'; let initialized = false; @@ -14,7 +14,7 @@ export const initErrorHandling = async () => { ErrorReporterProxy.error(error); }); - const dsn = config.getEnv('diagnostics.config.sentry.dsn'); + const dsn = Container.get(GlobalConfig).sentry.backendDsn; if (!dsn) { initialized = true; return; diff --git a/packages/cli/src/events/events.controller.ts b/packages/cli/src/events/events.controller.ts new file mode 100644 index 0000000000..994f803242 --- /dev/null +++ b/packages/cli/src/events/events.controller.ts @@ -0,0 +1,16 @@ +import { Get, RestController } from '@/decorators'; +import { AuthenticatedRequest } from '@/requests'; + +import { EventService } from './event.service'; + +/** This controller holds endpoints that the frontend uses to trigger telemetry events */ +@RestController('/events') +export class EventsController { + constructor(private readonly eventService: EventService) {} + + @Get('/session-started') + sessionStarted(req: AuthenticatedRequest) { + const pushRef = req.headers['push-ref']; + this.eventService.emit('session-started', { pushRef }); + } +} diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 7bdc0ac74d..5afe97f31a 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -49,6 +49,9 @@ export type AuthenticatedRequest< > = Omit, 'user' | 'cookies'> & { user: User; cookies: Record; + headers: express.Request['headers'] & { + 'push-ref': string; + }; }; // ---------------------------------- diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 08f74b2936..27ac3b09a1 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -1,4 +1,3 @@ -import type { FrontendSettings } from '@n8n/api-types'; import cookieParser from 'cookie-parser'; import express from 'express'; import { access as fsAccess } from 'fs/promises'; @@ -21,6 +20,7 @@ import { import { CredentialsOverwrites } from '@/credentials-overwrites'; import { ControllerRegistry } from '@/decorators'; import { MessageEventBus } from '@/eventbus/message-event-bus/message-event-bus'; +import { EventService } from '@/events/event.service'; import { LogStreamingEventRelay } from '@/events/log-streaming-event-relay'; import type { ICredentialsOverwrite } from '@/interfaces'; import { isLdapEnabled } from '@/ldap/helpers.ee'; @@ -58,12 +58,12 @@ import '@/controllers/user-settings.controller'; import '@/controllers/workflow-statistics.controller'; import '@/credentials/credentials.controller'; import '@/eventbus/event-bus.controller'; +import '@/events/events.controller'; import '@/executions/executions.controller'; import '@/external-secrets/external-secrets.controller.ee'; import '@/license/license.controller'; import '@/workflows/workflow-history/workflow-history.controller.ee'; import '@/workflows/workflows.controller'; -import { EventService } from './events/event.service'; @Service() export class Server extends AbstractServer { @@ -169,10 +169,6 @@ export class Server extends AbstractServer { const { frontendService } = this; if (frontendService) { - frontendService.addToSettings({ - versionCli: N8N_VERSION, - }); - await this.externalHooks.run('frontend.settings', [frontendService.getSettings()]); } @@ -244,11 +240,22 @@ export class Server extends AbstractServer { // Returns the current settings for the UI this.app.get( `/${this.restEndpoint}/settings`, - ResponseHelper.send( - async (req: express.Request): Promise => - frontendService.getSettings(req.headers['push-ref'] as string), - ), + ResponseHelper.send(async () => frontendService.getSettings()), ); + + // Return Sentry config as a static file + this.app.get(`/${this.restEndpoint}/sentry.js`, (_, res) => { + res.type('js'); + res.write('window.sentry='); + res.write( + JSON.stringify({ + dsn: this.globalConfig.sentry.frontendDsn, + environment: process.env.ENVIRONMENT || 'development', + release: N8N_VERSION, + }), + ); + res.end(); + }); } // ---------------------------------------- diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 1693aa939a..6ac3a1863c 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -10,11 +10,10 @@ import path from 'path'; import { Container, Service } from 'typedi'; import config from '@/config'; -import { LICENSE_FEATURES } from '@/constants'; +import { LICENSE_FEATURES, N8N_VERSION } from '@/constants'; import { CredentialTypes } from '@/credential-types'; import { CredentialsOverwrites } from '@/credentials-overwrites'; import { getVariablesLimit } from '@/environments/variables/environment-helpers'; -import { EventService } from '@/events/event.service'; import { getLdapLoginLabel } from '@/ldap/helpers.ee'; import { License } from '@/license'; import { LoadNodesAndCredentials } from '@/load-nodes-and-credentials'; @@ -47,7 +46,6 @@ export class FrontendService { private readonly mailer: UserManagementMailer, private readonly instanceSettings: InstanceSettings, private readonly urlService: UrlService, - private readonly eventService: EventService, ) { loadNodesAndCredentials.addPostProcessor(async () => await this.generateTypes()); void this.generateTypes(); @@ -102,7 +100,7 @@ export class FrontendService { urlBaseEditor: instanceBaseUrl, binaryDataMode: config.getEnv('binaryDataManager.mode'), nodeJsVersion: process.version.replace(/^v/, ''), - versionCli: '', + versionCli: N8N_VERSION, concurrency: config.getEnv('executions.concurrency.productionLimit'), authCookie: { secure: config.getEnv('secure_cookie'), @@ -242,9 +240,7 @@ export class FrontendService { this.writeStaticJSON('credentials', credentials); } - getSettings(pushRef?: string): FrontendSettings { - this.eventService.emit('session-started', { pushRef }); - + getSettings(): FrontendSettings { const restEndpoint = this.globalConfig.endpoints.rest; // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` @@ -344,10 +340,6 @@ export class FrontendService { return this.settings; } - addToSettings(newSettings: Record) { - this.settings = { ...this.settings, ...newSettings }; - } - private writeStaticJSON(name: string, data: INodeTypeBaseDescription[] | ICredentialType[]) { const { staticCacheDir } = this.instanceSettings; const filePath = path.join(staticCacheDir, `types/${name}.json`); diff --git a/packages/cli/src/workflows/workflows.controller.ts b/packages/cli/src/workflows/workflows.controller.ts index 57f46002c2..30ca5c9773 100644 --- a/packages/cli/src/workflows/workflows.controller.ts +++ b/packages/cli/src/workflows/workflows.controller.ts @@ -404,7 +404,7 @@ export class WorkflowsController { return await this.workflowExecutionService.executeManually( req.body, req.user, - req.headers['push-ref'] as string, + req.headers['push-ref'], req.query.partialExecutionVersion === '-1' ? config.getEnv('featureFlags.partialExecutionVersionDefault') : req.query.partialExecutionVersion, diff --git a/packages/editor-ui/index.html b/packages/editor-ui/index.html index 12dbfaeb9d..8176735fdd 100644 --- a/packages/editor-ui/index.html +++ b/packages/editor-ui/index.html @@ -9,6 +9,7 @@ window.BASE_PATH = '/{{BASE_PATH}}/'; window.REST_ENDPOINT = '{{REST_ENDPOINT}}'; + n8n.io - Workflow Automation diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 5e67b123d3..500b96affd 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -38,6 +38,7 @@ "@n8n/codemirror-lang": "workspace:*", "@n8n/codemirror-lang-sql": "^1.0.2", "@n8n/permissions": "workspace:*", + "@sentry/vue": "^8.31.0", "@vue-flow/background": "^1.3.0", "@vue-flow/controls": "^1.1.1", "@vue-flow/core": "^1.33.5", @@ -83,7 +84,7 @@ "@faker-js/faker": "^8.0.2", "@iconify/json": "^2.2.228", "@pinia/testing": "^0.1.3", - "@sentry/vite-plugin": "^2.5.0", + "@sentry/vite-plugin": "^2.22.4", "@types/dateformat": "^3.0.0", "@types/file-saver": "^2.0.1", "@types/humanize-duration": "^3.27.1", diff --git a/packages/editor-ui/src/api/events.ts b/packages/editor-ui/src/api/events.ts new file mode 100644 index 0000000000..00182bcaa1 --- /dev/null +++ b/packages/editor-ui/src/api/events.ts @@ -0,0 +1,6 @@ +import type { IRestApiContext } from '@/Interface'; +import { makeRestApiRequest } from '@/utils/apiUtils'; + +export async function sessionStarted(context: IRestApiContext): Promise { + return await makeRestApiRequest(context, 'GET', '/events/session-started'); +} diff --git a/packages/editor-ui/src/main.ts b/packages/editor-ui/src/main.ts index 66b7d6e279..30721aa209 100644 --- a/packages/editor-ui/src/main.ts +++ b/packages/editor-ui/src/main.ts @@ -1,4 +1,5 @@ import { createApp } from 'vue'; +import * as Sentry from '@sentry/vue'; import '@vue-flow/core/dist/style.css'; import '@vue-flow/core/dist/theme-default.css'; @@ -34,6 +35,11 @@ const pinia = createPinia(); const app = createApp(App); +if (window.sentry?.dsn) { + const { dsn, release, environment } = window.sentry; + Sentry.init({ app, dsn, release, environment }); +} + app.use(TelemetryPlugin); app.use(PiniaVuePlugin); app.use(I18nPlugin); diff --git a/packages/editor-ui/src/shims.d.ts b/packages/editor-ui/src/shims.d.ts index e0a9a886af..83cd8850cd 100644 --- a/packages/editor-ui/src/shims.d.ts +++ b/packages/editor-ui/src/shims.d.ts @@ -1,6 +1,7 @@ import type { VNode, ComponentPublicInstance } from 'vue'; import type { PartialDeep } from 'type-fest'; import type { ExternalHooks } from '@/types/externalHooks'; +import type { FrontendSettings } from '@n8n/api-types'; export {}; @@ -17,6 +18,7 @@ declare global { interface Window { BASE_PATH: string; REST_ENDPOINT: string; + sentry?: { dsn?: string; environment: string; release: string }; n8nExternalHooks?: PartialDeep; preventNodeViewBeforeUnload?: boolean; maxPinnedDataSize?: number; diff --git a/packages/editor-ui/vite.config.mts b/packages/editor-ui/vite.config.mts index f6824d31a5..80972dcd33 100644 --- a/packages/editor-ui/vite.config.mts +++ b/packages/editor-ui/vite.config.mts @@ -93,13 +93,13 @@ if (release && authToken) { sentryVitePlugin({ org: 'n8nio', project: 'instance-frontend', - // Specify the directory containing build artifacts - include: './dist', // Auth tokens can be obtained from https://sentry.io/settings/account/api/auth-tokens/ // and needs the `project:releases` and `org:read` scopes authToken, telemetry: false, - release, + release: { + name: release, + }, }), ); } diff --git a/patches/@sentry__cli@2.17.0.patch b/patches/@sentry__cli@2.36.2.patch similarity index 77% rename from patches/@sentry__cli@2.17.0.patch rename to patches/@sentry__cli@2.36.2.patch index f4325eaab4..04882a4fa0 100644 --- a/patches/@sentry__cli@2.17.0.patch +++ b/patches/@sentry__cli@2.36.2.patch @@ -1,5 +1,5 @@ diff --git a/js/helper.js b/js/helper.js -index a4a7a61f0e226d7cb45f8d5db34c35cc9e62cf96..38fd3ec2214970b1c5dd22fdff902ff3b5eeddde 100644 +index 37798b9444d39a8713327ed12adf3e76f03188a4..08d49b7a5c058c4d29c9929c0c524c85b14b330e 100644 --- a/js/helper.js +++ b/js/helper.js @@ -15,6 +15,7 @@ function getBinaryPath() { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c1a5be995..6ee921cd29 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -102,9 +102,9 @@ overrides: ws: '>=8.17.1' patchedDependencies: - '@sentry/cli@2.17.0': - hash: nchnoezkq6p37qaiku3vrpwraq - path: patches/@sentry__cli@2.17.0.patch + '@sentry/cli@2.36.2': + hash: saib6xuadkfhahfipsdedqib2i + path: patches/@sentry__cli@2.36.2.patch '@types/express-serve-static-core@4.17.43': hash: 5orrj4qleu2iko5t27vl44u4we path: patches/@types__express-serve-static-core@4.17.43.patch @@ -1325,6 +1325,9 @@ importers: '@n8n/permissions': specifier: workspace:* version: link:../@n8n/permissions + '@sentry/vue': + specifier: ^8.31.0 + version: 8.31.0(vue@3.4.21(typescript@5.6.2)) '@vue-flow/background': specifier: ^1.3.0 version: 1.3.0(@vue-flow/core@1.33.5(vue@3.4.21(typescript@5.6.2)))(vue@3.4.21(typescript@5.6.2)) @@ -1456,8 +1459,8 @@ importers: specifier: ^0.1.3 version: 0.1.3(pinia@2.1.6(typescript@5.6.2)(vue@3.4.21(typescript@5.6.2)))(vue@3.4.21(typescript@5.6.2)) '@sentry/vite-plugin': - specifier: ^2.5.0 - version: 2.5.0(encoding@0.1.13) + specifier: ^2.22.4 + version: 2.22.4(encoding@0.1.13) '@types/dateformat': specifier: ^3.0.0 version: 3.0.1 @@ -1908,10 +1911,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.2.1': - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} - '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -4050,16 +4049,81 @@ packages: '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@sentry-internal/browser-utils@8.31.0': + resolution: {integrity: sha512-Bq7TFMhPr1PixRGYkB/6ar9ws7sj224XzQ+hgpz6OxGEc9fQakvD8t/Nn7dp14k3FI/hcBRA6BBvpOKUUuPgGA==} + engines: {node: '>=14.18'} + + '@sentry-internal/feedback@8.31.0': + resolution: {integrity: sha512-R3LcC2IaTe8lgi5AU9h0rMgyVPpaTiMSLRhRlVeQPVmAKCz8pSG/um13q37t0BsXpTaImW9yYQ71Aj6h6IrShQ==} + engines: {node: '>=14.18'} + + '@sentry-internal/replay-canvas@8.31.0': + resolution: {integrity: sha512-ConyrhWozx4HluRj0+9teN4XTC1ndXjxMdJQvDnbLFsQhCCEdwUfaZVshV1CFe9T08Bfyjruaw33yR7pDXYktw==} + engines: {node: '>=14.18'} + + '@sentry-internal/replay@8.31.0': + resolution: {integrity: sha512-r8hmFDwWxeAxpdzBCRWTKQ/QHl8QanFw8XfM0fvFes/H1d/b43Vwc/IiUnsYoMOdooIP8hJFGDKlfq+Y5uVVGA==} + engines: {node: '>=14.18'} + '@sentry-internal/tracing@7.87.0': resolution: {integrity: sha512-HYa0+rfFmYQ/DadXoiuarTSxrcnYDCd/fm0pFuOHjICtfja8IcLegVYP2/r3CgwB+IjquCtJ5kDcqS/NTgUcpA==} engines: {node: '>=8'} - '@sentry/bundler-plugin-core@2.5.0': - resolution: {integrity: sha512-UNjeTSf0Irt/RsC1Xsfxa+RC92mBvjzuWQnvHJ5c+mXwUt+jLcFgGMCSVK/FCDZO+gAeKzmU1+G2UexbsNAP8w==} + '@sentry/babel-plugin-component-annotate@2.22.4': + resolution: {integrity: sha512-hbSq067KwmeKIEkmyzkTNJbmbtx2KRqvpiy9Q/DynI5Z46Nko/ppvgIfyFXK9DelwvEPOqZic4WXTIhO4iv3DA==} engines: {node: '>= 14'} - '@sentry/cli@2.17.0': - resolution: {integrity: sha512-CHIMEg8+YNCpEBDgUctu+DvG3S8+g8Zn9jTE5MMGINNmGkQTMG179LuDE04B/inaCYixLVNpFPTe6Iow3tXjnQ==} + '@sentry/browser@8.31.0': + resolution: {integrity: sha512-LZK0uLPGB4Al+qWc1eaad+H/1SR6CY9a0V2XWpUbNAT3+VkEo0Z/78bW1kb43N0cok87hNPOe+c66SfwdxphVQ==} + engines: {node: '>=14.18'} + + '@sentry/bundler-plugin-core@2.22.4': + resolution: {integrity: sha512-25NiyV3v6mdqOXlpzbbJnq0FHdAu1uTEDr+DU8CzNLjIXlq2Sr2CFZ/mhRcR6daM8OAretJdQ34lu0yHUVeE4Q==} + engines: {node: '>= 14'} + + '@sentry/cli-darwin@2.36.2': + resolution: {integrity: sha512-To64Pq+pcmecEr+gFXiqaZy8oKhyLQLXO/SVDdf16CUL2qpuahE3bO5h9kFacMxPPxOWcgc2btF+4gYa1+bQTA==} + engines: {node: '>=10'} + os: [darwin] + + '@sentry/cli-linux-arm64@2.36.2': + resolution: {integrity: sha512-g+FFmj1oJ2iRMsfs1ORz6THOO6MiAR55K9YxdZUBvqfoHLjSMt7Jst43sbZ3O0u55hnfixSKLNzDaTGaM/jxIQ==} + engines: {node: '>=10'} + cpu: [arm64] + os: [linux, freebsd] + + '@sentry/cli-linux-arm@2.36.2': + resolution: {integrity: sha512-cRSvOQK97WM0m03k/c+LVAWT042Qz887WP/2Gy64eUi/PfArwb+QZZnsu4FCygxK9jnzgLTo4+ewoJVi17xaLQ==} + engines: {node: '>=10'} + cpu: [arm] + os: [linux, freebsd] + + '@sentry/cli-linux-i686@2.36.2': + resolution: {integrity: sha512-rjxTw/CMd0Q7qlOb7gWFiwn3hJIxNkhbn1bOU54xj9CZvQSCvh10l7l4Y9o8znJLl41c5kMXVq8yuYws9A7AGQ==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [linux, freebsd] + + '@sentry/cli-linux-x64@2.36.2': + resolution: {integrity: sha512-cF8IPFTlwiC7JgVvSW4rS99sxb1W1N//iANxuzqaDswUnmJLi0AJy/jES87qE5GRB6ljaPVMvH7Kq0OCp3bvPA==} + engines: {node: '>=10'} + cpu: [x64] + os: [linux, freebsd] + + '@sentry/cli-win32-i686@2.36.2': + resolution: {integrity: sha512-YDH/Kcd8JAo1Bg4jtSwF8dr7FZZ8QbYLMx8q/5eenHpq6VdOgPENsTvayLW3cAjWLcm44u8Ed/gcEK0z1IxQmQ==} + engines: {node: '>=10'} + cpu: [x86, ia32] + os: [win32] + + '@sentry/cli-win32-x64@2.36.2': + resolution: {integrity: sha512-Kac8WPbkFSVAJqPAVRBiW0uij9PVoXo0owf+EDeIIDLs9yxZat0d1xgyQPlUWrCGdxowMSbDvaSUz1YnE7MUmg==} + engines: {node: '>=10'} + cpu: [x64] + os: [win32] + + '@sentry/cli@2.36.2': + resolution: {integrity: sha512-QoijP9TnO1UVNnRKtH718jlu/F9bBki6ffrOfmcjxkvLT6Q3nBMmqhYNH/AJV/RcgqLd6noWss4fbDMXZLzgIQ==} engines: {node: '>= 10'} hasBin: true @@ -4067,6 +4131,10 @@ packages: resolution: {integrity: sha512-jkoXqK/nuYh8DYS+n7uaSuSIdw4HJemyRkXsWjAEPtEgD7taGMafZGbP5pl+XE38SE59jTBxmKnkUEZOFMgZGA==} engines: {node: '>=8'} + '@sentry/core@8.31.0': + resolution: {integrity: sha512-5zsMBOML18e5a/ZoR5XpcYF59e2kSxb6lTg13u52f/+NA27EPgxKgXim5dz6L/6+0cizgwwmFaZFGJiFc2qoAA==} + engines: {node: '>=14.18'} + '@sentry/integrations@7.87.0': resolution: {integrity: sha512-xbyOQeyfG1sF2PBMIOz3c3i0Y3+8q4UlxoeOhpFe6Vpjek+I/g7onZT6YevT6cWG083cg+rS0VCgPQSUV2lxIw==} engines: {node: '>=8'} @@ -4075,26 +4143,32 @@ packages: resolution: {integrity: sha512-mGcZMCL3/IMTLIRcWLF+H9z2Bb2d34gKmg2rhXqI8BqhhUA551jMRlZv/y4za2Osjy550KwVoNsA1qtEe5mYyQ==} engines: {node: '>=8'} - '@sentry/types@7.60.1': - resolution: {integrity: sha512-8lKKSCOhZ953cWxwnfZwoR3ZFFlZG4P3PQFTaFt/u4LxLh/0zYbdtgvtUqXRURjMCi5P6ddeE9Uw9FGnTJCsTw==} - engines: {node: '>=8'} - '@sentry/types@7.87.0': resolution: {integrity: sha512-w8jKFHq/Llupmr2FezmFgQsnm3y/CnqLjb7s6PstI78E409wrhH7p7oqX/OEuzccH1qNCNwes/3QKvPTRQDB4Q==} engines: {node: '>=8'} - '@sentry/utils@7.60.1': - resolution: {integrity: sha512-ik+5sKGBx4DWuvf6UUKPSafaDiASxP+Xvjg3C9ppop2I/JWxP1FfZ5g22n5ZmPmNahD6clTSoTWly8qyDUlUOw==} - engines: {node: '>=8'} + '@sentry/types@8.31.0': + resolution: {integrity: sha512-prRM/n5nlP+xQZSpdEkSR8BwwZtgsLk0NbI8eCjTMu2isVlrlggop8pVaJb7y9HmElVtDA1Q6y4u8TD2htQKFQ==} + engines: {node: '>=14.18'} '@sentry/utils@7.87.0': resolution: {integrity: sha512-7xgtPTnTNP/4IznFMFXxltuaXfLvzznrYCDMv9ny8EeUjJqlLX3CVA8Qq3YALsLCQCKcrGRARbAcd/EGG//w2w==} engines: {node: '>=8'} - '@sentry/vite-plugin@2.5.0': - resolution: {integrity: sha512-u5lfIysy6UVzUGn/adyDcRXfzFyip4mLGThTnKeOeA9rCgmJUVnErH8TD8xhTKdpq/MBiQ+KqrC6xG9pKCa96g==} + '@sentry/utils@8.31.0': + resolution: {integrity: sha512-9W2LZ9QIHKc0HSyH/7UmTolc01Q4vX/qMSZk7i1noinlkQtnRUmTP39r1DSITjKCrDHj6zvB/J1RPDUoRcTXxQ==} + engines: {node: '>=14.18'} + + '@sentry/vite-plugin@2.22.4': + resolution: {integrity: sha512-C51PUlTv0BXN3+e9SjPHptNX3b9E0clrsaR5c//l/sFkQjuteDHKChA1gNzZSvfoa3gm9NzZAgpk3hVF2O3nBA==} engines: {node: '>= 14'} + '@sentry/vue@8.31.0': + resolution: {integrity: sha512-w512J2XLs43OZ7KBcdy4ho+IWMf37TQDJ5+JBONC+OLmGo7rixAZZxwIA7nI1/kZsBYEZ6JZL1uPCMrwwe/BsQ==} + engines: {node: '>=14.18'} + peerDependencies: + vue: 2.x || 3.x + '@sevinf/maybe@0.5.0': resolution: {integrity: sha512-ARhyoYDnY1LES3vYI0fiG6e9esWfTNcXcO6+MPJJXcnyMV3bim4lnFt45VXouV7y82F4x3YH8nOQ6VztuvUiWg==} @@ -9205,10 +9279,6 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true - magic-string@0.27.0: - resolution: {integrity: sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==} - engines: {node: '>=12'} - magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} @@ -12660,11 +12730,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.2.1': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -13908,18 +13973,18 @@ snapshots: '@babel/core@7.24.0': dependencies: - '@ampproject/remapping': 2.2.1 + '@ampproject/remapping': 2.3.0 '@babel/code-frame': 7.24.6 '@babel/generator': 7.23.6 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) '@babel/helpers': 7.24.0 - '@babel/parser': 7.24.6 + '@babel/parser': 7.25.6 '@babel/template': 7.24.0 '@babel/traverse': 7.24.0 - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 convert-source-map: 2.0.0 - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 7.6.0 @@ -13928,14 +13993,14 @@ snapshots: '@babel/generator@7.22.9': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 '@babel/generator@7.23.6': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@jridgewell/gen-mapping': 0.3.5 '@jridgewell/trace-mapping': 0.3.25 jsesc: 2.5.2 @@ -13953,15 +14018,15 @@ snapshots: '@babel/helper-function-name@7.23.0': dependencies: '@babel/template': 7.24.0 - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@babel/helper-hoist-variables@7.22.5': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@babel/helper-module-imports@7.22.15': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0)': dependencies: @@ -13970,17 +14035,17 @@ snapshots: '@babel/helper-module-imports': 7.22.15 '@babel/helper-simple-access': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.24.6 + '@babel/helper-validator-identifier': 7.24.7 '@babel/helper-plugin-utils@7.22.5': {} '@babel/helper-simple-access@7.22.5': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@babel/helper-split-export-declaration@7.22.6': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@babel/helper-string-parser@7.24.6': {} @@ -13998,7 +14063,7 @@ snapshots: dependencies: '@babel/template': 7.24.0 '@babel/traverse': 7.24.0 - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 transitivePeerDependencies: - supports-color @@ -14094,8 +14159,8 @@ snapshots: '@babel/template@7.24.0': dependencies: '@babel/code-frame': 7.24.6 - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@babel/traverse@7.24.0': dependencies: @@ -14105,9 +14170,9 @@ snapshots: '@babel/helper-function-name': 7.23.0 '@babel/helper-hoist-variables': 7.22.5 '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 - debug: 4.3.6(supports-color@8.1.1) + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 + debug: 4.3.7 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -15835,33 +15900,100 @@ snapshots: domhandler: 5.0.3 selderee: 0.11.0 + '@sentry-internal/browser-utils@8.31.0': + dependencies: + '@sentry/core': 8.31.0 + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + + '@sentry-internal/feedback@8.31.0': + dependencies: + '@sentry/core': 8.31.0 + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + + '@sentry-internal/replay-canvas@8.31.0': + dependencies: + '@sentry-internal/replay': 8.31.0 + '@sentry/core': 8.31.0 + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + + '@sentry-internal/replay@8.31.0': + dependencies: + '@sentry-internal/browser-utils': 8.31.0 + '@sentry/core': 8.31.0 + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + '@sentry-internal/tracing@7.87.0': dependencies: '@sentry/core': 7.87.0 '@sentry/types': 7.87.0 '@sentry/utils': 7.87.0 - '@sentry/bundler-plugin-core@2.5.0(encoding@0.1.13)': + '@sentry/babel-plugin-component-annotate@2.22.4': {} + + '@sentry/browser@8.31.0': dependencies: - '@sentry/cli': 2.17.0(patch_hash=nchnoezkq6p37qaiku3vrpwraq)(encoding@0.1.13) - '@sentry/node': 7.87.0 - '@sentry/utils': 7.60.1 + '@sentry-internal/browser-utils': 8.31.0 + '@sentry-internal/feedback': 8.31.0 + '@sentry-internal/replay': 8.31.0 + '@sentry-internal/replay-canvas': 8.31.0 + '@sentry/core': 8.31.0 + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + + '@sentry/bundler-plugin-core@2.22.4(encoding@0.1.13)': + dependencies: + '@babel/core': 7.24.0 + '@sentry/babel-plugin-component-annotate': 2.22.4 + '@sentry/cli': 2.36.2(patch_hash=saib6xuadkfhahfipsdedqib2i)(encoding@0.1.13) dotenv: 16.3.1 find-up: 5.0.0 glob: 9.3.2 - magic-string: 0.27.0 + magic-string: 0.30.8 unplugin: 1.0.1 transitivePeerDependencies: - encoding - supports-color - '@sentry/cli@2.17.0(patch_hash=nchnoezkq6p37qaiku3vrpwraq)(encoding@0.1.13)': + '@sentry/cli-darwin@2.36.2': + optional: true + + '@sentry/cli-linux-arm64@2.36.2': + optional: true + + '@sentry/cli-linux-arm@2.36.2': + optional: true + + '@sentry/cli-linux-i686@2.36.2': + optional: true + + '@sentry/cli-linux-x64@2.36.2': + optional: true + + '@sentry/cli-win32-i686@2.36.2': + optional: true + + '@sentry/cli-win32-x64@2.36.2': + optional: true + + '@sentry/cli@2.36.2(patch_hash=saib6xuadkfhahfipsdedqib2i)(encoding@0.1.13)': dependencies: https-proxy-agent: 5.0.1 node-fetch: 2.7.0(encoding@0.1.13) progress: 2.0.3 proxy-from-env: 1.1.0 which: 2.0.2 + optionalDependencies: + '@sentry/cli-darwin': 2.36.2 + '@sentry/cli-linux-arm': 2.36.2 + '@sentry/cli-linux-arm64': 2.36.2 + '@sentry/cli-linux-i686': 2.36.2 + '@sentry/cli-linux-x64': 2.36.2 + '@sentry/cli-win32-i686': 2.36.2 + '@sentry/cli-win32-x64': 2.36.2 transitivePeerDependencies: - encoding - supports-color @@ -15871,6 +16003,11 @@ snapshots: '@sentry/types': 7.87.0 '@sentry/utils': 7.87.0 + '@sentry/core@8.31.0': + dependencies: + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + '@sentry/integrations@7.87.0': dependencies: '@sentry/core': 7.87.0 @@ -15888,27 +16025,34 @@ snapshots: transitivePeerDependencies: - supports-color - '@sentry/types@7.60.1': {} - '@sentry/types@7.87.0': {} - '@sentry/utils@7.60.1': - dependencies: - '@sentry/types': 7.60.1 - tslib: 2.6.2 + '@sentry/types@8.31.0': {} '@sentry/utils@7.87.0': dependencies: '@sentry/types': 7.87.0 - '@sentry/vite-plugin@2.5.0(encoding@0.1.13)': + '@sentry/utils@8.31.0': dependencies: - '@sentry/bundler-plugin-core': 2.5.0(encoding@0.1.13) + '@sentry/types': 8.31.0 + + '@sentry/vite-plugin@2.22.4(encoding@0.1.13)': + dependencies: + '@sentry/bundler-plugin-core': 2.22.4(encoding@0.1.13) unplugin: 1.0.1 transitivePeerDependencies: - encoding - supports-color + '@sentry/vue@8.31.0(vue@3.4.21(typescript@5.6.2))': + dependencies: + '@sentry/browser': 8.31.0 + '@sentry/core': 8.31.0 + '@sentry/types': 8.31.0 + '@sentry/utils': 8.31.0 + vue: 3.4.21(typescript@5.6.2) + '@sevinf/maybe@0.5.0': {} '@sideway/address@4.1.5': @@ -17015,24 +17159,24 @@ snapshots: '@types/babel__core@7.20.0': dependencies: - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__generator': 7.6.4 '@types/babel__template': 7.4.1 '@types/babel__traverse': 7.18.2 '@types/babel__generator@7.6.4': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@types/babel__template@7.4.1': dependencies: - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 '@types/babel__traverse@7.18.2': dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@types/basic-auth@1.1.3': dependencies: @@ -18285,7 +18429,7 @@ snapshots: babel-plugin-jest-hoist@29.5.0: dependencies: '@babel/template': 7.24.0 - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 '@types/babel__core': 7.20.0 '@types/babel__traverse': 7.18.2 @@ -18313,7 +18457,7 @@ snapshots: babel-walk@3.0.0-canary-5: dependencies: - '@babel/types': 7.24.6 + '@babel/types': 7.25.6 balanced-match@1.0.2: {} @@ -18903,8 +19047,8 @@ snapshots: constantinople@4.0.1: dependencies: - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 content-disposition@0.5.4: dependencies: @@ -20599,7 +20743,7 @@ snapshots: fs.realpath: 1.0.0 minimatch: 7.4.2 minipass: 4.2.5 - path-scurry: 1.10.1 + path-scurry: 1.11.1 global-dirs@3.0.0: dependencies: @@ -21240,7 +21384,7 @@ snapshots: istanbul-lib-instrument@5.2.1: dependencies: '@babel/core': 7.24.0 - '@babel/parser': 7.24.6 + '@babel/parser': 7.25.6 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 semver: 7.6.0 @@ -21255,7 +21399,7 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.3.6(supports-color@8.1.1) + debug: 4.3.7 istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: @@ -22339,10 +22483,6 @@ snapshots: lz-string@1.5.0: {} - magic-string@0.27.0: - dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 - magic-string@0.30.10: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 @@ -26212,8 +26352,8 @@ snapshots: with@7.0.2: dependencies: - '@babel/parser': 7.24.6 - '@babel/types': 7.24.6 + '@babel/parser': 7.25.6 + '@babel/types': 7.25.6 assert-never: 1.2.1 babel-walk: 3.0.0-canary-5 From e0696080227aee7ccb50d51a82873e8a1ba4667d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Wed, 25 Sep 2024 08:38:15 +0200 Subject: [PATCH 09/29] fix(editor): Credentials scopes and n8n scopes mix up (#10930) --- .../src/components/CredentialEdit/CredentialEdit.vue | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue index 856f0c9315..ba3eb19ff0 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialEdit.vue @@ -280,8 +280,7 @@ const requiredPropertiesFilled = computed(() => { const credentialPermissions = computed(() => { return getResourcePermissions( - ((credentialId.value ? currentCredential.value : credentialData.value) as ICredentialsResponse) - ?.scopes, + (currentCredential.value as ICredentialsResponse)?.scopes ?? homeProject.value?.scopes, ).credential; }); @@ -341,11 +340,8 @@ onMounted(async () => { credentialTypeName: defaultCredentialTypeName.value, }); - const scopes = homeProject.value?.scopes ?? []; - credentialData.value = { ...credentialData.value, - scopes, ...(homeProject.value ? { homeProject: homeProject.value } : {}), }; } else { From 1ab94d60fd1a37dde52c647e5f58509b39f5ef9a Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 25 Sep 2024 08:48:23 +0100 Subject: [PATCH 10/29] feat(FTP Node): Add FTP node to Core Nodes > Other (no-changelog) (#10955) --- packages/nodes-base/nodes/Ftp/Ftp.node.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Ftp/Ftp.node.json b/packages/nodes-base/nodes/Ftp/Ftp.node.json index ddf134e4c8..2597d8f110 100644 --- a/packages/nodes-base/nodes/Ftp/Ftp.node.json +++ b/packages/nodes-base/nodes/Ftp/Ftp.node.json @@ -17,6 +17,6 @@ }, "alias": ["SFTP", "FTP", "Binary", "File", "Transfer"], "subcategories": { - "Core Nodes": ["Files"] + "Core Nodes": ["Files", "Helpers"] } } From 422c9463c8d931a728615a1fe5a10f05a96ecaa2 Mon Sep 17 00:00:00 2001 From: Jon Date: Wed, 25 Sep 2024 08:48:45 +0100 Subject: [PATCH 11/29] feat(Slack Node): Add option to hide workflow link on message update (#10927) --- .../nodes/Slack/V2/MessageDescription.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts index 837a53c26f..3100b258f5 100644 --- a/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts +++ b/packages/nodes-base/nodes/Slack/V2/MessageDescription.ts @@ -983,7 +983,30 @@ export const messageFields: INodeProperties[] = [ }, ], }, - + { + displayName: 'Options', + name: 'otherOptions', + type: 'collection', + displayOptions: { + show: { + operation: ['update'], + resource: ['message'], + }, + }, + default: {}, + description: 'Other options to set', + placeholder: 'Add option', + options: [ + { + displayName: 'Include Link to Workflow', + name: 'includeLinkToWorkflow', + type: 'boolean', + default: true, + description: + 'Whether to append a link to this workflow at the end of the message. This is helpful if you have many workflows sending Slack messages.', + }, + ], + }, /* ----------------------------------------------------------------------- */ /* message:delete /* ----------------------------------------------------------------------- */ From bdc0622f59e98c9e6c542f5cb59a2dbd9008ba96 Mon Sep 17 00:00:00 2001 From: Michael Kret <88898367+michael-radency@users.noreply.github.com> Date: Wed, 25 Sep 2024 11:15:10 +0300 Subject: [PATCH 12/29] feat: Page size 1 option (#10957) --- .../design-system/src/components/N8nDatatable/Datatable.vue | 2 +- packages/editor-ui/src/components/RunData.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/design-system/src/components/N8nDatatable/Datatable.vue b/packages/design-system/src/components/N8nDatatable/Datatable.vue index 3a28a4d54c..905aef4671 100644 --- a/packages/design-system/src/components/N8nDatatable/Datatable.vue +++ b/packages/design-system/src/components/N8nDatatable/Datatable.vue @@ -31,7 +31,7 @@ const emit = defineEmits<{ }>(); const { t } = useI18n(); -const rowsPerPageOptions = ref([10, 25, 50, 100]); +const rowsPerPageOptions = ref([1, 10, 25, 50, 100]); const $style = useCssModule(); diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index 870552d624..4885ee7ed9 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -200,7 +200,7 @@ export default defineComponent({ MAX_DISPLAY_ITEMS_AUTO_ALL, currentPage: 1, pageSize: 10, - pageSizes: [10, 25, 50, 100], + pageSizes: [1, 10, 25, 50, 100], pinDataDiscoveryTooltipVisible: false, isControlledPinDataTooltip: false, From a6cfb3b0c57dc7467204747422acc96131604e23 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 25 Sep 2024 11:28:07 +0300 Subject: [PATCH 13/29] feat(editor): Show strikethrough line for all single main input and single main output nodes in new canvas (no-changelog) (#10925) --- .../render-types/CanvasNodeDefault.spec.ts | 32 ++++++++++++++++++- .../nodes/render-types/CanvasNodeDefault.vue | 20 ++++++++++-- .../CanvasNodeDisabledStrikeThrough.spec.ts | 28 ++-------------- .../parts/CanvasNodeDisabledStrikeThrough.vue | 15 +-------- 4 files changed, 52 insertions(+), 43 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts index 084597e4d8..6ba17c31e7 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.spec.ts @@ -4,7 +4,7 @@ import { NodeConnectionType } from 'n8n-workflow'; import { createCanvasNodeProvide } from '@/__tests__/data'; import { createTestingPinia } from '@pinia/testing'; import { setActivePinia } from 'pinia'; -import { CanvasNodeRenderType } from '@/types'; +import { CanvasConnectionMode, CanvasNodeRenderType } from '@/types'; const renderComponent = createComponentRenderer(CanvasNodeDefault); @@ -158,6 +158,36 @@ describe('CanvasNodeDefault', () => { }); expect(getByText('Test Node').closest('.node')).not.toHaveClass('disabled'); }); + + it('should render strike-through when node is disabled and has node input and output handles', () => { + const { container } = renderComponent({ + global: { + provide: { + ...createCanvasNodeProvide({ + data: { + disabled: true, + inputs: [{ type: NodeConnectionType.Main, index: 0 }], + outputs: [{ type: NodeConnectionType.Main, index: 0 }], + connections: { + [CanvasConnectionMode.Input]: { + [NodeConnectionType.Main]: [ + [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], + ], + }, + [CanvasConnectionMode.Output]: { + [NodeConnectionType.Main]: [ + [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], + ], + }, + }, + }, + }), + }, + }, + }); + + expect(container.querySelector('.disabledStrikeThrough')).toBeVisible(); + }); }); describe('running', () => { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index bc443c1732..7a7f16f017 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -30,7 +30,14 @@ const { hasIssues, render, } = useCanvasNode(); -const { mainOutputs, mainInputs, nonMainInputs, requiredNonMainInputs } = useNodeConnections({ +const { + mainOutputs, + mainOutputConnections, + mainInputs, + mainInputConnections, + nonMainInputs, + requiredNonMainInputs, +} = useNodeConnections({ inputs, outputs, connections, @@ -86,6 +93,15 @@ const dataTestId = computed(() => { return `canvas-${type}-node`; }); +const isStrikethroughVisible = computed(() => { + const isSingleMainInputNode = + mainInputs.value.length === 1 && mainInputConnections.value.length <= 1; + const isSingleMainOutputNode = + mainOutputs.value.length === 1 && mainOutputConnections.value.length <= 1; + + return isDisabled.value && isSingleMainInputNode && isSingleMainOutputNode; +}); + function openContextMenu(event: MouseEvent) { emit('open:contextmenu', event); } @@ -103,7 +119,7 @@ function openContextMenu(event: MouseEvent) {
- +
{{ label }} diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts index 5375c552f6..57645abe91 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.spec.ts @@ -1,36 +1,12 @@ import CanvasNodeDisabledStrikeThrough from '@/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue'; import { createComponentRenderer } from '@/__tests__/render'; -import { NodeConnectionType } from 'n8n-workflow'; -import { createCanvasNodeProvide } from '@/__tests__/data'; -import { CanvasConnectionMode } from '@/types'; const renderComponent = createComponentRenderer(CanvasNodeDisabledStrikeThrough); describe('CanvasNodeDisabledStrikeThrough', () => { it('should render node correctly', () => { - const { container } = renderComponent({ - global: { - provide: { - ...createCanvasNodeProvide({ - data: { - connections: { - [CanvasConnectionMode.Input]: { - [NodeConnectionType.Main]: [ - [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], - ], - }, - [CanvasConnectionMode.Output]: { - [NodeConnectionType.Main]: [ - [{ node: 'node', type: NodeConnectionType.Main, index: 0 }], - ], - }, - }, - }, - }), - }, - }, - }); + const { container } = renderComponent(); - expect(container.firstChild).toBeVisible(); + expect(container.firstChild).toHaveClass('disabledStrikeThrough'); }); }); diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue index bd34aa025a..f006d10311 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/parts/CanvasNodeDisabledStrikeThrough.vue @@ -1,21 +1,8 @@ + + diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index f5e2999d7b..bd16fd6813 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -13,7 +13,6 @@ import { hasExpressionMapping, hasOnlyListMode, isValueExpression } from '@/util import { isResourceLocatorValue } from '@/utils/typeGuards'; import { createEventBus } from 'n8n-design-system/utils'; import type { INodeProperties, IParameterLabel, NodeParameterValueType } from 'n8n-workflow'; -import InlineExpressionTip from './InlineExpressionEditor/InlineExpressionTip.vue'; type Props = { parameter: INodeProperties; @@ -57,8 +56,7 @@ const ndvStore = useNDVStore(); const node = computed(() => ndvStore.activeNode); const hint = computed(() => i18n.nodeText().hint(props.parameter, props.path)); -const isInputTypeString = computed(() => props.parameter.type === 'string'); -const isInputTypeNumber = computed(() => props.parameter.type === 'number'); + const isResourceLocator = computed( () => props.parameter.type === 'resourceLocator' || props.parameter.type === 'workflowSelector', ); @@ -73,17 +71,6 @@ const isExpression = computed(() => isValueExpression(props.parameter, props.val const showExpressionSelector = computed(() => isResourceLocator.value ? !hasOnlyListMode(props.parameter) : true, ); -const isInputDataEmpty = computed(() => ndvStore.isNDVDataEmpty('input')); -const showDragnDropTip = computed( - () => - focused.value && - (isInputTypeString.value || isInputTypeNumber.value) && - !isExpression.value && - !isDropDisabled.value && - (!ndvStore.hasInputData || !isInputDataEmpty.value) && - !ndvStore.isMappingOnboarded && - ndvStore.isInputParentOfActiveNode, -); function onFocus() { focused.value = true; @@ -205,7 +192,7 @@ function onDrop(newParamValue: string) { -
- -
Date: Wed, 25 Sep 2024 15:50:50 +0300 Subject: [PATCH 25/29] fix(Google Sheets Node): Updating on row_number using automatic matching (#10940) --- .../Google/Sheet/test/v2/node/update.test.ts | 104 ++++++++++++++++++ .../v2/actions/sheet/update.operation.ts | 4 +- 2 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 packages/nodes-base/nodes/Google/Sheet/test/v2/node/update.test.ts diff --git a/packages/nodes-base/nodes/Google/Sheet/test/v2/node/update.test.ts b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/update.test.ts new file mode 100644 index 0000000000..899014a3c1 --- /dev/null +++ b/packages/nodes-base/nodes/Google/Sheet/test/v2/node/update.test.ts @@ -0,0 +1,104 @@ +import type { MockProxy } from 'jest-mock-extended'; +import { mock } from 'jest-mock-extended'; +import type { IExecuteFunctions, INode } from 'n8n-workflow'; + +import { execute } from '../../../v2/actions/sheet/update.operation'; +import type { GoogleSheet } from '../../../v2/helpers/GoogleSheet'; + +describe('Google Sheet - Update', () => { + let mockExecuteFunctions: MockProxy; + let mockGoogleSheet: MockProxy; + + beforeEach(() => { + mockExecuteFunctions = mock(); + mockGoogleSheet = mock(); + }); + + it('should update by row_number and not insert it as a new column', async () => { + mockExecuteFunctions.getInputData.mockReturnValueOnce([ + { + json: { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + pairedItem: { + item: 0, + input: undefined, + }, + }, + ]); + + mockExecuteFunctions.getNode.mockReturnValueOnce(mock({ typeVersion: 4.5 })); + mockExecuteFunctions.getNodeParameter + .mockReturnValueOnce('USER_ENTERED') // valueInputMode + .mockReturnValueOnce({}); // options + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce(['row_number']); // columnsToMatchOn + mockExecuteFunctions.getNodeParameter.mockReturnValueOnce('autoMapInputData'); // dataMode + + mockGoogleSheet.getData.mockResolvedValueOnce([ + ['id', 'name', 'text'], + ['1', 'a', 'a'], + ['2', 'x', 'x'], + ['3', 'b', 'b'], + ]); + + mockGoogleSheet.getColumnValues.mockResolvedValueOnce([]); + + mockGoogleSheet.prepareDataForUpdatingByRowNumber.mockReturnValueOnce({ + updateData: [ + { + range: 'Sheet1!B3', + values: [['NEW NAME']], + }, + { + range: 'Sheet1!C3', + values: [['NEW TEXT']], + }, + ], + }); + + mockGoogleSheet.batchUpdate.mockResolvedValueOnce([]); + + await execute.call(mockExecuteFunctions, mockGoogleSheet, 'Sheet1'); + + expect(mockGoogleSheet.getData).toHaveBeenCalledWith('Sheet1', 'FORMATTED_VALUE'); + expect(mockGoogleSheet.getColumnValues).toHaveBeenCalledWith({ + range: 'Sheet1!A:Z', + keyIndex: -1, + dataStartRowIndex: 1, + valueRenderMode: 'UNFORMATTED_VALUE', + sheetData: [ + ['id', 'name', 'text'], + ['1', 'a', 'a'], + ['2', 'x', 'x'], + ['3', 'b', 'b'], + ], + }); + expect(mockGoogleSheet.prepareDataForUpdatingByRowNumber).toHaveBeenCalledWith( + [ + { + row_number: 3, + name: 'NEW NAME', + text: 'NEW TEXT', + }, + ], + 'Sheet1!A:Z', + [['id', 'name', 'text']], + ); + + expect(mockGoogleSheet.batchUpdate).toHaveBeenCalledWith( + [ + { + range: 'Sheet1!B3', + values: [['NEW NAME']], + }, + { + range: 'Sheet1!C3', + values: [['NEW TEXT']], + }, + ], + 'USER_ENTERED', + ); + }); +}); diff --git a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts index 1d80ec0b82..16478cb11c 100644 --- a/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts +++ b/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/update.operation.ts @@ -307,11 +307,11 @@ export async function execute( if (handlingExtraDataOption === 'ignoreIt') { inputData.push(items[i].json); } - if (handlingExtraDataOption === 'error' && columnsToMatchOn[0] !== 'row_number') { + if (handlingExtraDataOption === 'error') { Object.keys(items[i].json).forEach((key) => errorOnUnexpectedColumn(key, i)); inputData.push(items[i].json); } - if (handlingExtraDataOption === 'insertInNewColumn' && columnsToMatchOn[0] !== 'row_number') { + if (handlingExtraDataOption === 'insertInNewColumn') { Object.keys(items[i].json).forEach(addNewColumn); inputData.push(items[i].json); } From af9e227ad4848995b9d82c72f814dbf9d1de506f Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Wed, 25 Sep 2024 15:25:26 +0200 Subject: [PATCH 26/29] feat(editor): Enable drag and drop in code editors (Code/SQL/HTML) (#10888) --- .../CodeNodeEditor/CodeNodeEditor.vue | 75 +++++++++++++++++-- .../src/components/ExpressionEditModal.vue | 4 +- .../components/ExpressionParameterInput.vue | 4 +- .../src/components/HtmlEditor/HtmlEditor.vue | 37 ++++++++- .../src/components/JsonEditor/JsonEditor.vue | 2 + .../src/components/SqlEditor/SqlEditor.vue | 40 +++++++++- .../src/plugins/codemirror/dragAndDrop.ts | 45 ++++++----- 7 files changed, 173 insertions(+), 34 deletions(-) diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index 381380cdfc..3ef1782b05 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -10,7 +10,7 @@ import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow'; import { format } from 'prettier'; import jsParser from 'prettier/plugins/babel'; import * as estree from 'prettier/plugins/estree'; -import { type Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'; +import { type Ref, computed, nextTick, onBeforeUnmount, onMounted, ref, toRaw, watch } from 'vue'; import { CODE_NODE_TYPE } from '@/constants'; import { codeNodeEditorEventBus } from '@/event-bus'; @@ -26,6 +26,7 @@ import { useLinter } from './linter'; import { codeNodeEditorTheme } from './theme'; import { useI18n } from '@/composables/useI18n'; import { useTelemetry } from '@/composables/useTelemetry'; +import { dropInCodeEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop'; type Props = { mode: CodeExecutionMode; @@ -51,6 +52,7 @@ const emit = defineEmits<{ const message = useMessage(); const editor = ref(null) as Ref; const languageCompartment = ref(new Compartment()); +const dragAndDropCompartment = ref(new Compartment()); const linterCompartment = ref(new Compartment()); const isEditorHovered = ref(false); const isEditorFocused = ref(false); @@ -95,6 +97,7 @@ onMounted(() => { extensions.push( ...writableEditorExtensions, + dragAndDropCompartment.value.of(dragAndDropExtension.value), EditorView.domEventHandlers({ focus: () => { isEditorFocused.value = true; @@ -151,6 +154,12 @@ const placeholder = computed(() => { return CODE_PLACEHOLDERS[props.language]?.[props.mode] ?? ''; }); +const dragAndDropEnabled = computed(() => { + return !props.isReadOnly && props.mode === 'runOnceForEachItem'; +}); + +const dragAndDropExtension = computed(() => (dragAndDropEnabled.value ? mappingDropCursor() : [])); + // eslint-disable-next-line vue/return-in-computed-property const languageExtensions = computed<[LanguageSupport, ...Extension[]]>(() => { switch (props.language) { @@ -188,6 +197,12 @@ watch( }, ); +watch(dragAndDropExtension, (extension) => { + editor.value?.dispatch({ + effects: dragAndDropCompartment.value.reconfigure(extension), + }); +}); + watch( () => props.language, (_newLanguage, previousLanguage: CodeNodeEditorLanguage) => { @@ -202,7 +217,6 @@ watch( reloadLinter(); }, ); - watch( aiEnabled, async (isEnabled) => { @@ -361,6 +375,12 @@ function onAiLoadStart() { function onAiLoadEnd() { isLoadingAIResponse.value = false; } + +async function onDrop(value: string, event: MouseEvent) { + if (!editor.value) return; + + await dropInCodeEditor(toRaw(editor.value), event, value); +}