🔀 Merge parent branch

This commit is contained in:
Iván Ovejero 2022-04-28 11:35:46 +02:00
commit e8e1e606b7
482 changed files with 5270 additions and 2762 deletions

View file

@ -1,3 +1,2 @@
packages/nodes-base
packages/editor-ui
packages/design-system

View file

@ -1,4 +1,6 @@
module.exports = {
root: true,
env: {
browser: true,
es6: true,
@ -21,6 +23,28 @@ module.exports = {
'**/migrations/**',
],
overrides: [
{
files: './packages/*(cli|core|workflow|node-dev)/**/*.ts',
plugins: [
/**
* Plugin with lint rules for import/export syntax
* https://github.com/import-js/eslint-plugin-import
*/
'eslint-plugin-import',
/**
* @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript
* See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins
*/
'@typescript-eslint',
/**
* Plugin to report formatting violations as lint violations
* https://github.com/prettier/eslint-plugin-prettier
*/
'eslint-plugin-prettier',
],
extends: [
/**
* Config for typescript-eslint recommended ruleset (without type checking)
@ -51,27 +75,6 @@ module.exports = {
*/
'eslint-config-prettier',
],
plugins: [
/**
* Plugin with lint rules for import/export syntax
* https://github.com/import-js/eslint-plugin-import
*/
'eslint-plugin-import',
/**
* @typescript-eslint/eslint-plugin is required by eslint-config-airbnb-typescript
* See step 2: https://github.com/iamturns/eslint-config-airbnb-typescript#2-install-eslint-plugins
*/
'@typescript-eslint',
/**
* Plugin to report formatting violations as lint violations
* https://github.com/prettier/eslint-plugin-prettier
*/
'eslint-plugin-prettier',
],
rules: {
// ******************************************************************
// required by prettier plugin
@ -122,7 +125,7 @@ module.exports = {
'undefined',
],
'no-void': ['error', { 'allowAsStatement': true }],
'no-void': ['error', { allowAsStatement: true }],
// ----------------------------------
// @typescript-eslint
@ -186,7 +189,10 @@ module.exports = {
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/explicit-member-accessibility.md
*/
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'no-public' }],
'@typescript-eslint/explicit-member-accessibility': [
'error',
{ accessibility: 'no-public' },
],
/**
* https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/member-delimiter-style.md
@ -363,4 +369,32 @@ module.exports = {
*/
'import/prefer-default-export': 'off',
},
},
{
files: ['./packages/nodes-base/nodes/**/*.ts'],
plugins: ['eslint-plugin-n8n-nodes-base'],
rules: {
"n8n-nodes-base/node-param-default-missing": "error",
"n8n-nodes-base/node-class-description-missing-subtitle": "error",
"n8n-nodes-base/node-class-description-inputs-wrong-trigger-node": "error",
"n8n-nodes-base/node-class-description-inputs-wrong-regular-node": "error",
"n8n-nodes-base/node-class-description-outputs-wrong": "error",
"n8n-nodes-base/node-execute-block-double-assertion-for-items": "error",
"n8n-nodes-base/node-param-default-wrong-for-collection": "error",
"n8n-nodes-base/node-param-default-wrong-for-boolean": "error",
"n8n-nodes-base/node-param-collection-type-unsorted-items": "error",
"n8n-nodes-base/node-param-default-wrong-for-fixed-collection": "error",
"n8n-nodes-base/node-param-default-wrong-for-multi-options": "error",
"n8n-nodes-base/node-param-description-excess-inner-whitespace": "error",
"n8n-nodes-base/node-param-description-empty-string": "error",
"n8n-nodes-base/node-param-description-comma-separated-hyphen": "error",
"n8n-nodes-base/node-param-default-wrong-for-simplify": "error",
"n8n-nodes-base/node-param-description-missing-for-return-all": "error",
"n8n-nodes-base/node-param-description-missing-final-period": "error",
"n8n-nodes-base/node-param-description-missing-for-simplify": "error",
"n8n-nodes-base/node-param-description-missing-for-ignore-ssl-issues": "error",
"n8n-nodes-base/node-param-description-identical-to-display-name": "error",
}
},
],
};

View file

@ -1,3 +1,48 @@
# [0.174.0](https://github.com/n8n-io/n8n/compare/n8n@0.173.1...n8n@0.174.0) (2022-04-25)
### Bug Fixes
- **core:** Open oauth callback endpoints to be public ([#3168](https://github.com/n8n-io/n8n/issues/3168)) ([01807d6](https://github.com/n8n-io/n8n/commit/01807d613654eb14dad0eb82defa4fab234a1d71))
- **MicrosoftOneDrive Node:** Fix issue with filenames that contain special characters from uploading ([#3183](https://github.com/n8n-io/n8n/issues/3183)) ([ff26a98](https://github.com/n8n-io/n8n/commit/ff26a987fe244b30f67d6516d84f1f43fed3ec43))
- **Slack Node:** Fix credential test ([#3151](https://github.com/n8n-io/n8n/issues/3151)) ([15e6d92](https://github.com/n8n-io/n8n/commit/15e6d9274ad0627dd5ebc30e70757878368042bc))
### Features
- **All AWS Nodes:** Enable support for AWS temporary credentials ([#2587](https://github.com/n8n-io/n8n/issues/2587)) ([ce79e6b](https://github.com/n8n-io/n8n/commit/ce79e6b74f6d94694f16988c8601f7c0639a04b3))
- **editor:** Add Workflow Stickies (Notes) ([#3154](https://github.com/n8n-io/n8n/issues/3154)) ([31dd01f](https://github.com/n8n-io/n8n/commit/31dd01f9cb7e1b6908a89c3402c78515a6475e61))
- **Google Sheets Node:** Add upsert support ([#2733](https://github.com/n8n-io/n8n/issues/2733)) ([aeb5a12](https://github.com/n8n-io/n8n/commit/aeb5a1234aa610b333525512085fe3b3bd60abef))
- **Microsoft Teams Node:** Enhancements and cleanup ([#2940](https://github.com/n8n-io/n8n/issues/2940)) ([d446f9e](https://github.com/n8n-io/n8n/commit/d446f9e28176e6ae2875d526cf4b6ac769dc750c))
- **MongoDB Node:** Allow parsing dates using dot notation ([#2487](https://github.com/n8n-io/n8n/issues/2487)) ([83998a1](https://github.com/n8n-io/n8n/commit/83998a15b0b4bea94aa07984136bdc56523d4f89))
# [0.173.1](https://github.com/n8n-io/n8n/compare/n8n@0.173.0...n8n@0.173.1) (2022-04-19)
### Bug Fixes
- **Discord Node:** Fix icon name
# [0.173.0](https://github.com/n8n-io/n8n/compare/n8n@0.172.0...n8n@0.173.0) (2022-04-19)
### Bug Fixes
- **core:** Add "rawBody" also for xml requests ([#3143](https://github.com/n8n-io/n8n/issues/3143)) ([5719e44](https://github.com/n8n-io/n8n/commit/5719e44b5999bb84e2fd50c8a469cc8934539747))
- **core:** Make email for UM case insensitive ([#3078](https://github.com/n8n-io/n8n/issues/3078)) ([8532b00](https://github.com/n8n-io/n8n/commit/8532b0030dbdeb85b2f74ce078adb44f43a7c4d3))
- **Discourse Node:** Fix issue with not all posts getting returned and add credential test ([#3007](https://github.com/n8n-io/n8n/issues/3007)) ([d68b7a4](https://github.com/n8n-io/n8n/commit/d68b7a4cf4b1025ce19e23f6bfc9e423595b6c0b))
- **editor:** Fix breaking Drop-downs after removing expressions ([#3094](https://github.com/n8n-io/n8n/issues/3094)) ([17b0cd8](https://github.com/n8n-io/n8n/commit/17b0cd8f765ce262241c827a635e64c189acc0f8))
- **Postgres Node:** Fix issue with columns containing spaces ([#2989](https://github.com/n8n-io/n8n/issues/2989)) ([0081d02](https://github.com/n8n-io/n8n/commit/0081d02b979ff5d98c5a834c60d8d8b5e83924ef))
- **ui:** Reset text-edit input value when pressing esc key to have matching input values ([#3098](https://github.com/n8n-io/n8n/issues/3098)) ([29fdd77](https://github.com/n8n-io/n8n/commit/29fdd77d7b4ac3bbb9faae73b0932183d48ae9a6))
- **ZendeskTrigger Node:** Fix deprecated targets, replaced with webhooks ([#3025](https://github.com/n8n-io/n8n/issues/3025)) ([794ad7c](https://github.com/n8n-io/n8n/commit/794ad7c756c68e0459d8f105acc3bcc1347d1e59))
- **Zoho Node:** Fix pagination issue ([#3129](https://github.com/n8n-io/n8n/issues/3129)) ([47bbe98](https://github.com/n8n-io/n8n/commit/47bbe9857b5f3321c9402595041afcb6b96411c4))
### Features
- **Discord Node:** Add additional options ([#2918](https://github.com/n8n-io/n8n/issues/2918)) ([310bffe](https://github.com/n8n-io/n8n/commit/310bffe7137f6baf36b93719c1e5abe8596dd346))
- **editor:** Add drag and drop from nodes panel ([#3123](https://github.com/n8n-io/n8n/issues/3123)) ([f566569](https://github.com/n8n-io/n8n/commit/f56656929992b98a3473944fd2a395e05d5c42f0))
- **Google Cloud Realtime Database Node:** Make it possible to select region ([#3096](https://github.com/n8n-io/n8n/issues/3096)) ([176538e](https://github.com/n8n-io/n8n/commit/176538e5f21f14ea3e5964dbe905fe4af89faaef))
- **GoogleBigQuery Node:** Add support for service account authentication ([#3128](https://github.com/n8n-io/n8n/issues/3128)) ([ac5f357](https://github.com/n8n-io/n8n/commit/ac5f357001b6887d649f65bc32a30e30aa75584b))
- **Markdown Node:** Add new node to covert between Markdown <> HTML ([#1728](https://github.com/n8n-io/n8n/issues/1728)) ([5d1ddb0](https://github.com/n8n-io/n8n/commit/5d1ddb0e9b56d999ec4d9278b81262aafceb43a9))
- **PagerDuty Node:** Add support for additional details in incidents ([#3140](https://github.com/n8n-io/n8n/issues/3140)) ([6ca7454](https://github.com/n8n-io/n8n/commit/6ca74540782623ac2301550b62f3382e88b8ed83)), closes [#3094](https://github.com/n8n-io/n8n/issues/3094) [#3105](https://github.com/n8n-io/n8n/issues/3105) [#3112](https://github.com/n8n-io/n8n/issues/3112) [#3078](https://github.com/n8n-io/n8n/issues/3078) [#3133](https://github.com/n8n-io/n8n/issues/3133) [#2918](https://github.com/n8n-io/n8n/issues/2918)
- **Slack Node:** Add blocks to slack message update ([#2182](https://github.com/n8n-io/n8n/issues/2182)) ([b5b6000](https://github.com/n8n-io/n8n/commit/b5b60008d680cd843a418390d451743fc13cac9c)), closes [#1728](https://github.com/n8n-io/n8n/issues/1728)
# [0.172.0](https://github.com/n8n-io/n8n/compare/n8n@0.171.1...n8n@0.172.0) (2022-04-11)
### Bug Fixes
@ -16,9 +61,9 @@
### Bug Fixes
* **core:** Fix issue with current executions not getting displayed ([#3093](https://github.com/n8n-io/n8n/issues/3093)) ([4af5168](https://github.com/n8n-io/n8n/commit/4af5168b3bc92578dc807bab1c11e3d90e151928))
* **core:** Fix issue with falsely skip authorizing ([#3087](https://github.com/n8n-io/n8n/issues/3087)) ([358a683](https://github.com/n8n-io/n8n/commit/358a683f381aa8eb7edd4886d6bdfe7ada61ec35))
* **WooCommerce Node:** Fix pagination issue with "Get All" operation ([#2529](https://github.com/n8n-io/n8n/issues/2529)) ([c2a5e0d](https://github.com/n8n-io/n8n/commit/c2a5e0d1b6a89cb7397b93bbb0f0be9be0df9c86))
- **core:** Fix issue with current executions not getting displayed ([#3093](https://github.com/n8n-io/n8n/issues/3093)) ([4af5168](https://github.com/n8n-io/n8n/commit/4af5168b3bc92578dc807bab1c11e3d90e151928))
- **core:** Fix issue with falsely skip authorizing ([#3087](https://github.com/n8n-io/n8n/issues/3087)) ([358a683](https://github.com/n8n-io/n8n/commit/358a683f381aa8eb7edd4886d6bdfe7ada61ec35))
- **WooCommerce Node:** Fix pagination issue with "Get All" operation ([#2529](https://github.com/n8n-io/n8n/issues/2529)) ([c2a5e0d](https://github.com/n8n-io/n8n/commit/c2a5e0d1b6a89cb7397b93bbb0f0be9be0df9c86))
# [0.171.0](https://github.com/n8n-io/n8n/compare/n8n@0.170.0...n8n@0.171.0) (2022-04-03)

2337
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.168.2",
"version": "0.174.0",
"private": true,
"homepage": "https://n8n.io",
"scripts": {

View file

@ -1,6 +1,6 @@
{
"name": "n8n",
"version": "0.172.0",
"version": "0.174.0",
"description": "n8n Workflow Automation Tool",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -125,10 +125,10 @@
"lodash.get": "^4.4.2",
"lodash.merge": "^4.6.2",
"mysql2": "~2.3.0",
"n8n-core": "~0.113.0",
"n8n-editor-ui": "~0.139.0",
"n8n-nodes-base": "~0.170.0",
"n8n-workflow": "~0.95.0",
"n8n-core": "~0.115.0",
"n8n-editor-ui": "~0.141.0",
"n8n-nodes-base": "~0.172.0",
"n8n-workflow": "~0.97.0",
"nodemailer": "^6.7.1",
"oauth-1.0a": "^2.2.6",
"open": "^7.0.0",

View file

@ -6,7 +6,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { Credentials, NodeExecuteFunctions } from 'n8n-core';
// eslint-disable-next-line import/no-extraneous-dependencies
import { get } from 'lodash';
import { NodeVersionedType } from 'n8n-nodes-base';
import {
@ -450,7 +451,7 @@ export class CredentialsHelper extends ICredentialsHelper {
// eslint-disable-next-line @typescript-eslint/await-thenable
const credentials = await this.getCredentials(nodeCredentials, type);
if (Db.collections.Credentials === null) {
if (!Db.isInitialized) {
// The first time executeWorkflow gets called the Database has
// to get initialized first
await Db.init();
@ -660,8 +661,10 @@ export class CredentialsHelper extends ICredentialsHelper {
mode,
);
let response: INodeExecutionData[][] | null | undefined;
try {
await routingNode.runNode(
response = await routingNode.runNode(
inputData,
runIndex,
nodeTypeCopy,
@ -710,6 +713,24 @@ export class CredentialsHelper extends ICredentialsHelper {
};
}
if (
credentialTestFunction.testRequest.rules &&
Array.isArray(credentialTestFunction.testRequest.rules)
) {
// Special testing rules are defined so check all in order
for (const rule of credentialTestFunction.testRequest.rules) {
if (rule.type === 'responseSuccessBody') {
const responseData = response![0][0].json;
if (get(responseData, rule.properties.key) === rule.properties.value) {
return {
status: 'Error',
message: rule.properties.message,
};
}
}
}
}
return {
status: 'OK',
message: 'Connection successful!',
@ -759,3 +780,13 @@ export async function getCredentialForUser(
return sharedCredential.credentials as ICredentialsDb;
}
/**
* Get a credential without user check
*/
export async function getCredentialWithoutUser(
credentialId: string,
): Promise<ICredentialsDb | undefined> {
const credential = await Db.collections.Credentials.findOne(credentialId);
return credential;
}

View file

@ -84,11 +84,18 @@ export class InternalHooksClass implements IInternalHooksClass {
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
const notesCount = Object.keys(nodeGraph.notes).length;
const overlappingCount = Object.values(nodeGraph.notes).filter(
(note) => note.overlapping,
).length;
return this.telemetry.track('User saved workflow', {
user_id: userId,
workflow_id: workflow.id,
node_graph: nodeGraph,
node_graph_string: JSON.stringify(nodeGraph),
notes_count_overlapping: overlappingCount,
notes_count_non_overlapping: notesCount - overlappingCount,
version_cli: this.versionCli,
num_tags: workflow.tags?.length ?? 0,
});

View file

@ -137,6 +137,7 @@ import {
WorkflowHelpers,
WorkflowRunner,
getCredentialForUser,
getCredentialWithoutUser,
} from '.';
import config from '../config';
@ -1823,18 +1824,18 @@ class App {
LoggerProxy.error(
'OAuth1 callback failed because of insufficient parameters received',
{
userId: req.user.id,
userId: req.user?.id,
credentialId,
},
);
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
const credential = await getCredentialForUser(credentialId, req.user);
const credential = await getCredentialWithoutUser(credentialId);
if (!credential) {
LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
userId: req.user.id,
userId: req.user?.id,
credentialId,
});
const errorResponse = new ResponseHelper.ResponseError(
@ -1884,7 +1885,7 @@ class App {
oauthToken = await requestPromise(options);
} catch (error) {
LoggerProxy.error('Unable to fetch tokens for OAuth1 callback', {
userId: req.user.id,
userId: req.user?.id,
credentialId,
});
const errorResponse = new ResponseHelper.ResponseError(
@ -1914,13 +1915,13 @@ class App {
await Db.collections.Credentials!.update(credentialId, newCredentialsData);
LoggerProxy.verbose('OAuth1 callback successful for new credential', {
userId: req.user.id,
userId: req.user?.id,
credentialId,
});
res.sendFile(pathResolve(__dirname, '../../templates/oauth-callback.html'));
} catch (error) {
LoggerProxy.error('OAuth1 callback failed because of insufficient user permissions', {
userId: req.user.id,
userId: req.user?.id,
credentialId: req.query.cid,
});
// Error response
@ -2087,11 +2088,11 @@ class App {
return ResponseHelper.sendErrorResponse(res, errorResponse);
}
const credential = await getCredentialForUser(state.cid, req.user);
const credential = await getCredentialWithoutUser(state.cid);
if (!credential) {
LoggerProxy.error('OAuth2 callback failed because of insufficient permissions', {
userId: req.user.id,
userId: req.user?.id,
credentialId: state.cid,
});
const errorResponse = new ResponseHelper.ResponseError(
@ -2132,7 +2133,7 @@ class App {
!token.verify(decryptedDataOriginal.csrfSecret as string, state.token)
) {
LoggerProxy.debug('OAuth2 callback state is invalid', {
userId: req.user.id,
userId: req.user?.id,
credentialId: state.cid,
});
const errorResponse = new ResponseHelper.ResponseError(
@ -2183,7 +2184,7 @@ class App {
if (oauthToken === undefined) {
LoggerProxy.error('OAuth2 callback failed: unable to get access tokens', {
userId: req.user.id,
userId: req.user?.id,
credentialId: state.cid,
});
const errorResponse = new ResponseHelper.ResponseError(
@ -2217,7 +2218,7 @@ class App {
// Save the credentials in DB
await Db.collections.Credentials!.update(state.cid, newCredentialsData);
LoggerProxy.verbose('OAuth2 callback successful for new credential', {
userId: req.user.id,
userId: req.user?.id,
credentialId: state.cid,
});

View file

@ -66,6 +66,8 @@ export function addRoutes(this: N8nApp, ignoredEndpoints: string[], restEndpoint
req.url.startsWith(`/${restEndpoint}/forgot-password`) ||
req.url.startsWith(`/${restEndpoint}/resolve-password-token`) ||
req.url.startsWith(`/${restEndpoint}/change-password`) ||
req.url.startsWith(`/${restEndpoint}/oauth2-credential/callback`) ||
req.url.startsWith(`/${restEndpoint}/oauth1-credential/callback`) ||
isAuthExcluded(req.url, ignoredEndpoints)
) {
return next();

View file

@ -300,7 +300,7 @@ class App {
}
this.app.use((req: express.Request, res: express.Response, next: express.NextFunction) => {
if (Db.collections.Workflow === null) {
if (!Db.isInitialized) {
const error = new ResponseHelper.ResponseError('Database is not ready!', undefined, 503);
return ResponseHelper.sendErrorResponse(res, error);
}

View file

@ -789,7 +789,7 @@ export async function getWorkflowData(
let workflowData: IWorkflowBase | undefined;
if (workflowInfo.id !== undefined) {
if (Db.collections.Workflow === null) {
if (!Db.isInitialized) {
// The first time executeWorkflow gets called the Database has
// to get initialized first
await Db.init();

View file

@ -237,7 +237,9 @@ export declare namespace OAuthRequest {
{},
{},
{ oauth_verifier: string; oauth_token: string; cid: string }
>;
> & {
user?: User;
};
}
namespace OAuth2Credential {

View file

@ -13,6 +13,7 @@ import {
} from './shared/random';
import * as testDb from './shared/testDb';
import type { Role } from '../../src/databases/entities/Role';
import { SMTP_TEST_TIMEOUT } from './shared/constants';
jest.mock('../../src/telemetry');
@ -40,19 +41,22 @@ beforeEach(async () => {
config.set('userManagement.isInstanceOwnerSetUp', true);
config.set('userManagement.emails.mode', '');
jest.setTimeout(30000); // fake SMTP service might be slow
});
afterAll(async () => {
await testDb.terminate(testDbName);
});
test('POST /forgot-password should send password reset email', async () => {
test(
'POST /forgot-password should send password reset email',
async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authlessAgent = utils.createAgent(app);
const member = await testDb.createUser({ email: 'test@test.com', globalRole: globalMemberRole });
const member = await testDb.createUser({
email: 'test@test.com',
globalRole: globalMemberRole,
});
await utils.configureSmtp();
@ -68,7 +72,9 @@ test('POST /forgot-password should send password reset email', async () => {
expect(user.resetPasswordTokenExpiration).toBeGreaterThan(Math.ceil(Date.now() / 1000));
}),
);
});
},
SMTP_TEST_TIMEOUT,
);
test('POST /forgot-password should fail if emailing is not set up', async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });

View file

@ -57,3 +57,8 @@ export const BOOTSTRAP_POSTGRES_CONNECTION_NAME: Readonly<string> = 'n8n_bs_post
* for each suite test run.
*/
export const BOOTSTRAP_MYSQL_CONNECTION_NAME: Readonly<string> = 'n8n_bs_mysql';
/**
* Timeout (in milliseconds) to account for fake SMTP service being slow to respond.
*/
export const SMTP_TEST_TIMEOUT = 30_000;

View file

@ -4,7 +4,7 @@ import { v4 as uuid } from 'uuid';
import { Db } from '../../src';
import config from '../../config';
import { SUCCESS_RESPONSE_BODY } from './shared/constants';
import { SMTP_TEST_TIMEOUT, SUCCESS_RESPONSE_BODY } from './shared/constants';
import {
randomEmail,
randomValidPassword,
@ -47,8 +47,6 @@ beforeAll(async () => {
utils.initTestTelemetry();
utils.initTestLogger();
jest.setTimeout(30000); // fake SMTP service might be slow
});
beforeEach(async () => {
@ -481,7 +479,9 @@ test('POST /users should fail if user management is disabled', async () => {
expect(response.statusCode).toBe(500);
});
test('POST /users should email invites and create user shells but ignore existing', async () => {
test(
'POST /users should email invites and create user shells but ignore existing',
async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const member = await testDb.createUser({ globalRole: globalMemberRole });
const memberShell = await testDb.createUserShell(globalMemberRole);
@ -489,7 +489,12 @@ test('POST /users should email invites and create user shells but ignore existin
await utils.configureSmtp();
const testEmails = [randomEmail(), randomEmail().toUpperCase(), memberShell.email, member.email];
const testEmails = [
randomEmail(),
randomEmail().toUpperCase(),
memberShell.email,
member.email,
];
const payload = testEmails.map((e) => ({ email: e }));
@ -522,9 +527,13 @@ test('POST /users should email invites and create user shells but ignore existin
expect(password).toBeNull();
expect(resetPasswordToken).toBeNull();
}
});
},
SMTP_TEST_TIMEOUT,
);
test('POST /users should fail with invalid inputs', async () => {
test(
'POST /users should fail with invalid inputs',
async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
@ -547,9 +556,13 @@ test('POST /users should fail with invalid inputs', async () => {
expect(users.length).toBe(1); // DB unaffected
}),
);
});
},
SMTP_TEST_TIMEOUT,
);
test('POST /users should ignore an empty payload', async () => {
test(
'POST /users should ignore an empty payload',
async () => {
const owner = await testDb.createUser({ globalRole: globalOwnerRole });
const authOwnerAgent = utils.createAgent(app, { auth: true, user: owner });
@ -565,7 +578,9 @@ test('POST /users should ignore an empty payload', async () => {
const users = await Db.collections.User!.find();
expect(users.length).toBe(1);
});
},
SMTP_TEST_TIMEOUT,
);
// TODO: /users/:id/reinvite route tests missing

View file

@ -1,6 +1,6 @@
{
"name": "n8n-core",
"version": "0.113.0",
"version": "0.115.0",
"description": "Core functionality of n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -52,7 +52,7 @@
"form-data": "^4.0.0",
"lodash.get": "^4.4.2",
"mime-types": "^2.1.27",
"n8n-workflow": "~0.95.0",
"n8n-workflow": "~0.97.0",
"oauth-1.0a": "^2.2.6",
"p-cancelable": "^2.0.0",
"qs": "^6.10.1",

View file

@ -1,6 +1,6 @@
{
"name": "n8n-design-system",
"version": "0.17.0",
"version": "0.18.0",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
"author": {
@ -79,6 +79,7 @@
"vue-loader": "^15.9.7",
"vue-property-decorator": "^9.1.2",
"vue-template-compiler": "^2.6.11",
"vue-typed-mixins": "^0.2.0",
"vue2-boring-avatars": "0.3.4",
"xss": "^1.0.10"
}

View file

@ -69,7 +69,6 @@ export default {
default: false,
},
icon: {
type: String,
},
round: {
type: Boolean,

View file

@ -26,7 +26,6 @@ export default {
},
props: {
icon: {
type: String,
required: true,
},
size: {

View file

@ -40,7 +40,6 @@ export default {
default: false,
},
icon: {
type: String,
required: true,
},
theme: {

View file

@ -1,6 +1,10 @@
<template>
<div>
<div v-if="!loading" ref="editor" :class="$style.markdown" v-html="htmlContent" />
<div
v-if="!loading"
ref="editor"
:class="$style[theme]" v-html="htmlContent"
/>
<div v-else :class="$style.markdown">
<div v-for="(block, index) in loadingBlocks"
:key="index">
@ -59,6 +63,9 @@ export default {
content: {
type: String,
},
withMultiBreaks: {
type: Boolean,
},
images: {
type: Array,
},
@ -75,6 +82,10 @@ export default {
return 3;
},
},
theme: {
type: String,
default: 'markdown',
},
options: {
type: Object,
default() {
@ -106,7 +117,11 @@ export default {
}
const fileIdRegex = new RegExp('fileId:([0-9]+)');
const html = this.md.render(escapeMarkdown(this.content));
let contentToRender = this.content;
if (this.withMultiBreaks) {
contentToRender = contentToRender.replaceAll('\n\n', '\n&nbsp;\n');
}
const html = this.md.render(escapeMarkdown(contentToRender));
const safeHtml = xss(html, {
onTagAttr: (tag, name, value, isWhiteAttr) => {
if (tag === 'img' && name === 'src') {
@ -214,6 +229,67 @@ export default {
}
}
.sticky {
color: var(--color-text-dark);
h1, h2, h3, h4 {
margin-bottom: var(--spacing-2xs);
font-weight: var(--font-weight-bold);
line-height: var(--font-line-height-loose);
}
h1 {
font-size: 36px;
}
h2 {
font-size: 24px;
}
h3, h4, h5, h6 {
font-size: var(--font-size-m);
}
p {
margin-bottom: var(--spacing-2xs);
font-size: var(--font-size-s);
font-weight: var(--font-weight-regular);
line-height: var(--font-line-height-loose);
}
ul, ol {
margin-bottom: var(--spacing-2xs);
padding-left: var(--spacing-m);
li {
margin-top: 0.25em;
font-size: var(--font-size-s);
font-weight: var(--font-weight-regular);
line-height: var(--font-line-height-regular);
}
}
code {
background-color: var(--color-background-base);
padding: 0 var(--spacing-4xs);
color: var(--color-secondary);
}
pre > code,li > code, p > code {
color: var(--color-secondary);
}
a {
&:hover {
text-decoration: underline;
}
}
img {
object-fit: contain;
}
}
.spacer {
margin: var(--spacing-2xl);
}

View file

@ -0,0 +1,238 @@
<template>
<div :class="$style.resize">
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="right" :class="[$style.resizer, $style.right]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="left" :class="[$style.resizer, $style.left]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top" :class="[$style.resizer, $style.top]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom" :class="[$style.resizer, $style.bottom]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-left" :class="[$style.resizer, $style.topLeft]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-right" :class="[$style.resizer, $style.topRight]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-left" :class="[$style.resizer, $style.bottomLeft]" />
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-right" :class="[$style.resizer, $style.bottomRight]" />
<slot></slot>
</div>
</template>
<script lang="ts">
const cursorMap = {
right: 'ew-resize',
top: 'ns-resize',
bottom: 'ns-resize',
left: 'ew-resize',
'top-left': 'nw-resize',
'top-right' : 'ne-resize',
'bottom-left': 'sw-resize',
'bottom-right': 'se-resize',
};
function closestNumber(value: number, divisor: number): number {
let q = parseInt(value / divisor);
let n1 = divisor * q;
let n2 = (value * divisor) > 0 ?
(divisor * (q + 1)) : (divisor * (q - 1));
if (Math.abs(value - n1) < Math.abs(value - n2))
return n1;
return n2;
}
function getSize(delta, min, virtual, gridSize): number {
const target = closestNumber(virtual, gridSize);
if (target >= min && virtual > 0) {
return target;
}
return min;
};
export default {
name: 'n8n-resize',
props: {
isResizingEnabled: {
type: Boolean,
default: true,
},
height: {
type: Number,
},
width: {
type: Number,
},
minHeight: {
type: Number,
},
minWidth: {
type: Number,
},
scale: {
type: Number,
default: 1,
},
gridSize: {
type: Number,
},
},
data() {
return {
dir: '',
dHeight: 0,
dWidth: 0,
vHeight: 0,
vWidth: 0,
x: 0,
y: 0,
};
},
methods: {
resizerMove(e) {
e.preventDefault();
e.stopPropagation();
const targetResizer = e.target;
this.dir = targetResizer.dataset.dir;
document.body.style.cursor = cursorMap[this.dir];
this.x = e.pageX;
this.y = e.pageY;
this.dWidth = 0;
this.dHeight = 0;
this.vHeight = this.height;
this.vWidth = this.width;
window.addEventListener('mousemove', this.mouseMove);
window.addEventListener('mouseup', this.mouseUp);
this.$emit('resizestart');
},
mouseMove(e) {
e.preventDefault();
e.stopPropagation();
let dWidth = 0;
let dHeight = 0;
let top = false;
let left = false;
if (this.dir.includes('right')) {
dWidth = e.pageX - this.x;
}
if (this.dir.includes('left')) {
dWidth = this.x - e.pageX;
left = true;
}
if (this.dir.includes('top')) {
dHeight = this.y - e.pageY;
top = true;
}
if (this.dir.includes('bottom')) {
dHeight = e.pageY - this.y;
}
const deltaWidth = (dWidth - this.dWidth) / this.scale;
const deltaHeight = (dHeight - this.dHeight) / this.scale;
this.vHeight = this.vHeight + deltaHeight;
this.vWidth = this.vWidth + deltaWidth;
const height = getSize(deltaHeight, this.minHeight, this.vHeight, this.gridSize);
const width = getSize(deltaWidth, this.minWidth, this.vWidth, this.gridSize);
const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
const dY = top && height !== this.height ? -1 * (height - this.height): 0;
this.$emit('resize', { height, width, dX, dY });
this.dHeight = dHeight;
this.dWidth = dWidth;
},
mouseUp(e) {
e.preventDefault();
e.stopPropagation();
this.$emit('resizeend');
window.removeEventListener('mousemove', this.mouseMove);
window.removeEventListener('mouseup', this.mouseUp);
document.body.style.cursor = 'unset';
this.dir = '';
},
},
};
</script>
<style lang="scss" module>
.resize {
position: absolute;
width: 100%;
height: 100%;
z-index: 2;
}
.resizer {
position: absolute;
z-index: 2;
}
.right {
width: 12px;
height: 100%;
top: -2px;
right: -2px;
cursor: ew-resize;
}
.top {
width: 100%;
height: 12px;
top: -2px;
left: -2px;
cursor: ns-resize;
}
.bottom {
width: 100%;
height: 12px;
bottom: -2px;
left: -2px;
cursor: ns-resize;
}
.left {
width: 12px;
height: 100%;
top: -2px;
left: -2px;
cursor: ew-resize;
}
.topLeft {
width: 12px;
height: 12px;
top: -3px;
left: -3px;
cursor: nw-resize;
z-index: 3;
}
.topRight {
width: 12px;
height: 12px;
top: -3px;
right: -3px;
cursor: ne-resize;
z-index: 3;
}
.bottomLeft {
width: 12px;
height: 12px;
bottom: -3px;
left: -3px;
cursor: sw-resize;
z-index: 3;
}
.bottomRight {
width: 12px;
height: 12px;
bottom: -3px;
right: -3px;
cursor: se-resize;
z-index: 3;
}
</style>

View file

@ -0,0 +1,67 @@
import { action } from '@storybook/addon-actions';
import N8nSticky from './Sticky.vue';
export default {
title: 'Atoms/Sticky',
component: N8nSticky,
argTypes: {
content: {
control: {
control: 'text',
},
},
height: {
control: {
control: 'number',
},
},
minHeight: {
control: {
control: 'number',
},
},
minWidth: {
control: {
control: 'number',
},
},
readOnly: {
control: {
control: 'Boolean',
},
},
width: {
control: {
control: 'number',
},
},
},
};
const methods = {
onInput: action('input'),
onResize: action('resize'),
onResizeEnd: action('resizeend'),
onResizeStart: action('resizestart'),
};
const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
components: {
N8nSticky,
},
template:
'<n8n-sticky v-bind="$props" @resize="onResize" @resizeend="onResizeEnd" @resizeStart="onResizeStart" @input="onInput"></n8n-sticky>',
methods,
});
export const Sticky = Template.bind({});
Sticky.args = {
height: 160,
width: 150,
content: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
defaultText: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
minHeight: 80,
minWidth: 150,
readOnly: false,
};

View file

@ -0,0 +1,253 @@
<template>
<div
:class="{[$style.sticky]: true, [$style.clickable]: !isResizing}"
:style="styles"
@keydown.prevent
>
<resize
:isResizingEnabled="!readOnly"
:height="height"
:width="width"
:minHeight="minHeight"
:minWidth="minWidth"
:scale="scale"
:gridSize="gridSize"
@resizeend="onResizeEnd"
@resize="onResize"
@resizestart="onResizeStart"
>
<template>
<div
v-show="!editMode"
:class="$style.wrapper"
@dblclick.stop="onDoubleClick"
>
<n8n-markdown
theme="sticky"
:content="content"
:withMultiBreaks="true"
/>
</div>
<div
v-show="editMode"
@click.stop
@mousedown.stop
@mouseup.stop
@keydown.esc="onInputBlur"
@keydown.stop
@wheel.stop
class="sticky-textarea"
:class="{'full-height': !shouldShowFooter}"
>
<n8n-input
:value="content"
type="textarea"
:rows="5"
@blur="onInputBlur"
@input="onInput"
ref="input"
/>
</div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
<n8n-text
size="xsmall"
aligh="right"
>
<span v-html="t('sticky.markdownHint')"></span>
</n8n-text>
</div>
</template>
</resize>
</div>
</template>
<script lang="ts">
import N8nInput from '../N8nInput';
import N8nMarkdown from '../N8nMarkdown';
import Resize from './Resize';
import N8nText from '../N8nText';
import Locale from '../../mixins/locale';
import mixins from 'vue-typed-mixins';
export default mixins(Locale).extend({
name: 'n8n-sticky',
props: {
content: {
type: String,
},
height: {
type: Number,
default: 180,
},
width: {
type: Number,
default: 240,
},
minHeight: {
type: Number,
default: 80,
},
minWidth: {
type: Number,
default: 150,
},
scale: {
type: Number,
default: 1,
},
gridSize: {
type: Number,
default: 20,
},
id: {
type: String,
default: '0',
},
defaultText: {
type: String,
},
editMode: {
type: Boolean,
default: false,
},
readOnly: {
type: Boolean,
default: false,
},
},
components: {
N8nInput,
N8nMarkdown,
Resize,
N8nText,
},
data() {
return {
isResizing: false,
};
},
computed: {
resHeight(): number {
if (this.height < this.minHeight) {
return this.minHeight;
}
return this.height;
},
resWidth(): number {
if (this.width < this.minWidth) {
return this.minWidth;
}
return this.width;
},
styles() {
return {
height: this.resHeight + 'px',
width: this.resWidth + 'px',
};
},
shouldShowFooter() {
return this.resHeight > 100 && this.resWidth > 155;
},
},
methods: {
onDoubleClick() {
if (!this.readOnly) {
this.$emit('edit', true);
}
},
onInputBlur(value) {
if (!this.isResizing) {
this.$emit('edit', false);
}
},
onInput(value: string) {
this.$emit('input', value);
},
onResize(values) {
this.$emit('resize', values);
},
onResizeEnd(resizeEnd) {
this.isResizing = false;
this.$emit('resizeend', resizeEnd);
},
onResizeStart() {
this.isResizing = true;
this.$emit('resizestart');
},
},
watch: {
editMode(newMode, prevMode) {
setTimeout(() => {
if (newMode && !prevMode && this.$refs.input && this.$refs.input.$refs && this.$refs.input.$refs.textarea) {
const textarea = this.$refs.input.$refs.textarea;
if (this.defaultText === this.content) {
textarea.select();
}
textarea.focus();
}
}, 100);
},
},
});
</script>
<style lang="scss" module>
.sticky {
position: absolute;
background-color: var(--color-sticky-default-background);
border: 1px solid var(--color-sticky-default-border);
border-radius: var(--border-radius-base);
}
.clickable {
cursor: pointer;
}
.wrapper {
width: 100%;
height: 100%;
position: absolute;
padding: var(--spacing-2xs) var(--spacing-xs) 0;
overflow: hidden;
&::after {
content: '';
width: 100%;
height: 24px;
left: 0;
bottom: 0;
position: absolute;
background: linear-gradient(180deg, var(--color-sticky-default-background), #fff5d600 0.01%, var(--color-sticky-default-background));
border-radius: var(--border-radius-base);
}
}
.footer {
padding: var(--spacing-5xs) var(--spacing-2xs) 0 var(--spacing-2xs);
display: flex;
justify-content: flex-end;
}
</style>
<style lang="scss">
.sticky-textarea {
height: calc(100% - var(--spacing-l));
padding: var(--spacing-2xs) var(--spacing-2xs) 0 var(--spacing-2xs);
cursor: default;
.el-textarea {
height: 100%;
.el-textarea__inner {
height: 100%;
resize: unset;
}
}
}
.full-height {
height: calc(100% - var(--spacing-2xs));
}
</style>

View file

@ -0,0 +1,3 @@
import Sticky from './Sticky.vue';
export default Sticky;

View file

@ -57,6 +57,7 @@ import N8nOption from './N8nOption';
import N8nRadioButtons from './N8nRadioButtons';
import N8nSelect from './N8nSelect';
import N8nSpinner from './N8nSpinner';
import N8nSticky from './N8nSticky';
import N8nSquareButton from './N8nSquareButton';
import N8nTags from './N8nTags';
import N8nTabs from './N8nTabs';
@ -93,6 +94,7 @@ export {
N8nRadioButtons,
N8nSelect,
N8nSpinner,
N8nSticky,
N8nSquareButton,
N8nTabs,
N8nTags,

View file

@ -16,4 +16,5 @@ export default {
config.minimum > 1 ? 's' : ''
}`),
"formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter",
"sticky.markdownHint": `You can style with <a href="https://docs.n8n.io/workflows/sticky-notes/" target="_blank">Markdown</a>`,
};

View file

@ -166,3 +166,17 @@ import ColorCircles from './ColorCircles.vue';
}}
</Story>
</Canvas>
## Sticky
<Canvas>
<Story name="sticky">
{{
template: `<color-circles :colors="['--color-sticky-default-background', '--color-sticky-default-border']" />`,
components: {
ColorCircles,
},
}}
</Story>
</Canvas>

View file

@ -3,6 +3,7 @@ export const escapeMarkdown = (html: string | undefined): string => {
return '';
}
const escaped = html.replace(/</g, "&lt;").replace(/>/g, "&gt;");
// unescape greater than quotes at start of line
const withQuotes = escaped.replace(/^((\s)*(&gt;)+)+\s*/gm, (matches) => {
return matches.replace(/&gt;/g, '>');

View file

@ -70,13 +70,6 @@
var(--color-secondary-l)
);
--color-secondary-tint-1-l: 92%;
--color-secondary-tint-1: hsl(
var(--color-secondary-h),
var(--color-secondary-s),
var(--color-secondary-tint-1-l)
);
--color-success-h: 150.4;
--color-success-s: 60%;
--color-success-l: 40.4%;
@ -340,6 +333,24 @@
--color-json-line: #bfcbd9;
--color-json-highlight: #E2E5EE;
--color-sticky-default-background-h: 46;
--color-sticky-default-background-s: 100%;
--color-sticky-default-background-l: 92%;
--color-sticky-default-background: hsl(
var(--color-sticky-default-background-h),
var(--color-sticky-default-background-s),
var(--color-sticky-default-background-l)
);
--color-sticky-default-border-h: 43;
--color-sticky-default-border-s: 75%;
--color-sticky-default-border-l: 80%;
--color-sticky-default-border: hsl(
var(--color-sticky-default-border-h),
var(--color-sticky-default-border-s),
var(--color-sticky-default-border-l)
);
--border-radius-xlarge: 12px;
--border-radius-large: 8px;
--border-radius-base: 4px;

View file

@ -1,6 +1,6 @@
{
"name": "n8n-editor-ui",
"version": "0.139.0",
"version": "0.141.0",
"description": "Workflow Editor UI for n8n",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -25,14 +25,15 @@
},
"dependencies": {
"@fontsource/open-sans": "^4.5.0",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"luxon": "^2.3.0",
"n8n-design-system": "~0.17.0",
"monaco-editor": "^0.29.1",
"n8n-design-system": "~0.18.0",
"timeago.js": "^4.0.2",
"v-click-outside": "^3.1.2",
"vue-fragment": "^1.5.2",
"vue2-boring-avatars": "0.3.4",
"vue-i18n": "^8.26.7",
"vue2-boring-avatars": "0.3.4",
"xss": "^1.0.10"
},
"devDependencies": {
@ -77,7 +78,7 @@
"lodash.debounce": "^4.0.8",
"lodash.get": "^4.4.2",
"lodash.set": "^4.3.2",
"n8n-workflow": "~0.95.0",
"n8n-workflow": "~0.97.0",
"monaco-editor-webpack-plugin": "^5.0.0",
"normalize-wheel": "^1.0.1",
"prismjs": "^1.17.1",

View file

@ -1,6 +1,6 @@
<template>
<el-dialog
:visible="!!node || renaming"
:visible="(!!node || renaming) && !isActiveStickyNode"
:before-close="close"
:show-close="false"
custom-class="data-display-wrapper"
@ -41,6 +41,7 @@ import RunData from '@/components/RunData.vue';
import mixins from 'vue-typed-mixins';
import Vue from 'vue';
import { mapGetters } from 'vuex';
import { STICKY_NODE_TYPE } from '@/constants';
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
name: 'DataDisplay',
@ -76,15 +77,18 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
}
return null;
},
isActiveStickyNode(): boolean {
return !!this.$store.getters.activeNode && this.$store.getters.activeNode.type === STICKY_NODE_TYPE;
},
},
watch: {
node (node, oldNode) {
if(node && !oldNode) {
if(node && !oldNode && !this.isActiveStickyNode) {
this.triggerWaitingWarningEnabled = false;
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
}
if (window.top) {
if (window.top && !this.isActiveStickyNode) {
window.top.postMessage(JSON.stringify({command: (node? 'openNDV': 'closeNDV')}), '*');
}
},

View file

@ -5,7 +5,7 @@
clickable: props.clickable,
active: props.active,
}"
@click="listeners['click']"
@click="listeners.click"
>
<CategoryItem
v-if="props.item.type === 'category'"
@ -21,7 +21,9 @@
v-else-if="props.item.type === 'node'"
:nodeType="props.item.properties.nodeType"
:bordered="!props.lastNode"
></NodeItem>
@dragstart="listeners.dragstart"
@dragend="listeners.dragend"
/>
</div>
</template>

View file

@ -8,7 +8,12 @@
@before-leave="beforeLeave"
@leave="leave"
>
<div v-for="(item, index) in elements" :key="item.key" :class="item.type" :data-key="item.key">
<div
v-for="(item, index) in elements"
:key="item.key"
:class="item.type"
:data-key="item.key"
>
<CreatorItem
:item="item"
:active="activeIndex === index && !disabled"
@ -16,7 +21,9 @@
:lastNode="
index === elements.length - 1 || elements[index + 1].type !== 'node'
"
@click="() => selected(item)"
@click="$emit('selected', item)"
@dragstart="emit('dragstart', item, $event)"
@dragend="emit('dragend', item, $event)"
/>
</div>
</div>
@ -36,12 +43,12 @@ export default Vue.extend({
},
props: ['elements', 'activeIndex', 'disabled', 'transitionsEnabled'],
methods: {
selected(element: INodeCreateElement) {
emit(eventName: string, element: INodeCreateElement, event: Event) {
if (this.$props.disabled) {
return;
}
this.$emit('selected', element);
this.$emit(eventName, { element, event });
},
beforeEnter(el: HTMLElement) {
el.style.height = '0';

View file

@ -1,7 +1,18 @@
<template>
<div @click="onClickInside" class="container">
<div
class="container"
ref="mainPanelContainer"
@click="onClickInside"
>
<SlideTransition>
<SubcategoryPanel v-if="activeSubcategory" :elements="subcategorizedNodes" :title="activeSubcategory.properties.subcategory" :activeIndex="activeSubcategoryIndex" @close="onSubcategoryClose" @selected="selected" />
<SubcategoryPanel
v-if="activeSubcategory"
:elements="subcategorizedNodes"
:title="activeSubcategory.properties.subcategory"
:activeIndex="activeSubcategoryIndex"
@close="onSubcategoryClose"
@selected="selected"
/>
</SlideTransition>
<div class="main-panel">
<SearchBar
@ -35,7 +46,10 @@
@selected="selected"
/>
</div>
<NoResults v-else @nodeTypeSelected="nodeTypeSelected" />
<NoResults
v-else
@nodeTypeSelected="$emit('nodeTypeSelected', $event)"
/>
</div>
</div>
</template>
@ -56,7 +70,6 @@ import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE
import SlideTransition from '../transitions/SlideTransition.vue';
import { matchesNodeType, matchesSelectType } from './helpers';
export default mixins(externalHooks).extend({
name: 'NodeCreateList',
components: {
@ -235,18 +248,13 @@ export default mixins(externalHooks).extend({
},
selected(element: INodeCreateElement) {
if (element.type === 'node') {
const properties = element.properties as INodeItemProps;
this.nodeTypeSelected(properties.nodeType.name);
this.$emit('nodeTypeSelected', (element.properties as INodeItemProps).nodeType.name);
} else if (element.type === 'category') {
this.onCategorySelected(element.category);
} else if (element.type === 'subcategory') {
this.onSubcategorySelected(element);
}
},
nodeTypeSelected(nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
onCategorySelected(category: string) {
if (this.activeCategory.includes(category)) {
this.activeCategory = this.activeCategory.filter(

View file

@ -1,8 +1,20 @@
<template>
<div>
<SlideTransition>
<div class="node-creator" v-if="active" v-click-outside="onClickOutside">
<MainPanel @nodeTypeSelected="nodeTypeSelected" :categorizedItems="categorizedItems" :categoriesWithNodes="categoriesWithNodes" :searchItems="searchItems"></MainPanel>
<div
v-if="active"
class="node-creator"
ref="nodeCreator"
v-click-outside="onClickOutside"
@dragover="onDragOver"
@drop="onDrop"
>
<MainPanel
@nodeTypeSelected="nodeTypeSelected"
:categorizedItems="categorizedItems"
:categoriesWithNodes="categoriesWithNodes"
:searchItems="searchItems"
/>
</div>
</SlideTransition>
</div>
@ -94,6 +106,22 @@ export default Vue.extend({
nodeTypeSelected (nodeTypeName: string) {
this.$emit('nodeTypeSelected', nodeTypeName);
},
onDragOver(event: DragEvent) {
event.preventDefault();
},
onDrop(event: DragEvent) {
if (!event.dataTransfer) {
return;
}
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
const nodeCreatorBoundingRect = (this.$refs.nodeCreator as Element).getBoundingClientRect();
// Abort drag end event propagation if dropped inside nodes panel
if (nodeTypeName && event.pageX >= nodeCreatorBoundingRect.x && event.pageY >= nodeCreatorBoundingRect.y) {
event.stopPropagation();
}
},
},
watch: {
nodeTypes(newList) {

View file

@ -1,5 +1,10 @@
<template>
<div :class="{[$style['node-item']]: true, [$style.bordered]: bordered}">
<div
draggable
@dragstart="onDragStart"
@dragend="onDragEnd"
:class="{[$style['node-item']]: true, [$style.bordered]: bordered}"
>
<NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
<div>
<div :class="$style.details">
@ -11,7 +16,7 @@
}}
</span>
<span :class="$style['trigger-icon']">
<TriggerIcon v-if="$options.isTrigger(nodeType)" />
<TriggerIcon v-if="isTrigger" />
</span>
</div>
<div :class="$style.description">
@ -21,14 +26,26 @@
})
}}
</div>
<div :class="$style['draggable-data-transfer']" ref="draggableDataTransfer" />
<transition name="node-item-transition">
<div
:class="$style.draggable"
:style="draggableStyle"
ref="draggable"
v-show="dragging"
>
<NodeIcon class="node-icon" :nodeType="nodeType" :size="40" :shrink="false" />
</div>
</transition>
</div>
</div>
</template>
<script lang="ts">
import {getNewNodePosition, NODE_SIZE} from '@/views/canvasHelpers';
import Vue from 'vue';
import { INodeTypeDescription } from 'n8n-workflow';
import NodeIcon from '../NodeIcon.vue';
import TriggerIcon from '../TriggerIcon.vue';
@ -44,14 +61,73 @@ export default Vue.extend({
'nodeType',
'bordered',
],
data() {
return {
dragging: false,
draggablePosition: {
x: -100,
y: -100,
},
};
},
computed: {
shortNodeType() {
shortNodeType(): string {
return this.$locale.shortNodeType(this.nodeType.name);
},
isTrigger (): boolean {
return this.nodeType.group.includes('trigger');
},
draggableStyle(): { top: string; left: string; } {
return {
top: `${this.draggablePosition.y}px`,
left: `${this.draggablePosition.x}px`,
};
},
},
mounted() {
/**
* Workaround for firefox, that doesn't attach the pageX and pageY coordinates to "ondrag" event.
* All browsers attach the correct page coordinates to the "dragover" event.
* @bug https://bugzilla.mozilla.org/show_bug.cgi?id=505521
*/
document.body.addEventListener("dragover", this.onDragOver);
},
destroyed() {
document.body.removeEventListener("dragover", this.onDragOver);
},
methods: {
onDragStart(event: DragEvent): void {
const { pageX: x, pageY: y } = event;
this.$emit('dragstart', event);
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = "copy";
event.dataTransfer.dropEffect = "copy";
event.dataTransfer.setData('nodeTypeName', this.nodeType.name);
event.dataTransfer.setDragImage(this.$refs.draggableDataTransfer as Element, 0, 0);
}
this.dragging = true;
this.draggablePosition = { x, y };
},
onDragOver(event: DragEvent): void {
if (!this.dragging || event.pageX === 0 && event.pageY === 0) {
return;
}
const [x,y] = getNewNodePosition([], [event.pageX - NODE_SIZE / 2, event.pageY - NODE_SIZE / 2]);
this.draggablePosition = { x, y };
},
onDragEnd(event: DragEvent): void {
this.$emit('dragend', event);
this.dragging = false;
setTimeout(() => {
this.draggablePosition = { x: -100, y: -100 };
}, 300);
},
// @ts-ignore
isTrigger (nodeType: INodeTypeDescription): boolean {
return nodeType.group.includes('trigger');
},
});
</script>
@ -100,4 +176,39 @@ export default Vue.extend({
display: flex;
}
.draggable {
width: 100px;
height: 100px;
position: fixed;
z-index: 1;
opacity: 0.66;
border: 2px solid var(--color-foreground-xdark);
border-radius: var(--border-radius-large);
background-color: var(--color-background-xlight);
display: flex;
justify-content: center;
align-items: center;
}
.draggable-data-transfer {
width: 1px;
height: 1px;
}
</style>
<style lang="scss" scoped>
.node-item-transition {
&-enter-active,
&-leave-active {
transition-property: opacity, transform;
transition-duration: 300ms;
transition-timing-function: ease;
}
&-enter,
&-leave-to {
opacity: 0;
transform: scale(0);
}
}
</style>

View file

@ -13,7 +13,9 @@
<ItemIterator
:elements="elements"
:activeIndex="activeIndex"
@selected="selected"
@selected="$emit('selected', $event)"
@dragstart="$emit('dragstart', $event)"
@dragend="$emit('dragend', $event)"
/>
</div>
</div>
@ -38,9 +40,6 @@ export default Vue.extend({
},
},
methods: {
selected(element: INodeCreateElement) {
this.$emit('selected', element);
},
onBackArrowClick() {
this.$emit('close');
},

View file

@ -0,0 +1,288 @@
<template>
<div class="sticky-wrapper" :style="stickyPosition">
<div
:class="{'sticky-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}"
:style="stickySize"
>
<div class="select-sticky-background" v-show="isSelected" />
<div
class="sticky-box"
:data-name="data.name"
:ref="data.name"
@click.left="mouseLeftClick"
v-touch:start="touchStart"
v-touch:end="touchEnd"
>
<n8n-sticky
:content.sync="node.parameters.content"
:height="node.parameters.height"
:width="node.parameters.width"
:scale="nodeViewScale"
:id="nodeIndex"
:readOnly="isReadOnly"
:defaultText="defaultText"
:editMode="isActive && !isReadOnly"
:gridSize="gridSize"
@input="onInputChange"
@edit="onEdit"
@resizestart="onResizeStart"
@resize="onResize"
@resizeend="onResizeEnd"
/>
</div>
<div v-show="showActions" class="sticky-options no-select-on-click">
<div v-touch:tap="deleteNode" class="option" :title="$locale.baseText('node.deleteNode')" >
<font-awesome-icon icon="trash" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import mixins from 'vue-typed-mixins';
import { externalHooks } from '@/components/mixins/externalHooks';
import { nodeBase } from '@/components/mixins/nodeBase';
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import { getStyleTokenValue, isNumber, isString } from './helpers';
import { INodeUi, XYPosition } from '@/Interface';
import {
INodeTypeDescription,
} from 'n8n-workflow';
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
name: 'Sticky',
props: {
nodeViewScale: {
type: Number,
},
gridSize: {
type: Number,
},
},
computed: {
defaultText (): string {
if (!this.nodeType) {
return '';
}
const properties = this.nodeType.properties;
const content = properties.find((property) => property.name === 'content');
return content && isString(content.default) ? content.default : '';
},
isSelected (): boolean {
return this.$store.getters.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name);
},
nodeType (): INodeTypeDescription | null {
return this.data && this.$store.getters.nodeType(this.data.type, this.data.typeVersion);
},
node (): INodeUi | undefined { // same as this.data but reactive..
return this.$store.getters.nodesByName[this.name] as INodeUi | undefined;
},
position (): XYPosition {
if (this.node) {
return this.node.position;
} else {
return [0, 0];
}
},
height(): number {
return this.node && isNumber(this.node.parameters.height)? this.node.parameters.height : 0;
},
width(): number {
return this.node && isNumber(this.node.parameters.width)? this.node.parameters.width : 0;
},
stickySize(): object {
const returnStyles: {
[key: string]: string | number;
} = {
height: this.height + 'px',
width: this.width + 'px',
};
return returnStyles;
},
stickyPosition (): object {
const returnStyles: {
[key: string]: string | number;
} = {
left: this.position[0] + 'px',
top: this.position[1] + 'px',
zIndex: this.isActive ? 9999999 : -1 * Math.floor((this.height * this.width) / 1000),
};
return returnStyles;
},
showActions(): boolean {
return !(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing);
},
workflowRunning (): boolean {
return this.$store.getters.isActionActive('workflowRunning');
},
},
data () {
return {
isResizing: false,
isTouchActive: false,
};
},
methods: {
deleteNode () {
Vue.nextTick(() => {
// Wait a tick else vue causes problems because the data is gone
this.$emit('removeNode', this.data.name);
});
},
onEdit(edit: boolean) {
if (edit && !this.isActive && this.node) {
this.$store.commit('setActiveNode', this.node.name);
}
else if (this.isActive && !edit) {
this.$store.commit('setActiveNode', null);
}
},
onInputChange(content: string) {
this.setParameters({content});
},
onResizeStart() {
this.isResizing = true;
if (!this.isSelected && this.node) {
this.$emit('nodeSelected', this.node.name, false, true);
}
const nodeIndex = this.$store.getters.getNodeIndex(this.data.name);
const nodeIdName = `node-${nodeIndex}`;
this.instance.destroyDraggable(nodeIdName); // todo
},
onResize({height, width, dX, dY}: { width: number, height: number, dX: number, dY: number }) {
if (!this.node) {
return;
}
if (dX !== 0 || dY !== 0) {
this.setPosition([this.node.position[0] + (dX || 0), this.node.position[1] + (dY || 0)]);
}
this.setParameters({ height, width });
},
onResizeEnd() {
this.isResizing = false;
this.__makeInstanceDraggable(this.data);
},
setParameters(params: {content?: string, height?: number, width?: number}) {
if (this.node) {
const nodeParameters = {
content: isString(params.content) ? params.content : this.node.parameters.content,
height: isNumber(params.height) ? params.height : this.node.parameters.height,
width: isNumber(params.width) ? params.width : this.node.parameters.width,
};
const updateInformation = {
name: this.node.name,
value: nodeParameters,
};
this.$store.commit('setNodeParameters', updateInformation);
}
},
setPosition(position: XYPosition) {
if (!this.node) {
return;
}
const updateInformation = {
name: this.node.name,
properties: {
position,
},
};
this.$store.commit('updateNodeProperties', updateInformation);
},
touchStart () {
if (this.isTouchDevice === true && this.isMacOs === false && this.isTouchActive === false) {
this.isTouchActive = true;
setTimeout(() => {
this.isTouchActive = false;
}, 2000);
}
},
},
});
</script>
<style lang="scss" scoped>
.sticky-wrapper {
position: absolute;
.sticky-default {
position: absolute;
.sticky-box {
width: 100%;
height: 100%;
}
&.touch-active,
&:hover {
.sticky-options {
display: flex;
cursor: pointer;
}
}
.sticky-options {
display: none;
justify-content: flex-start;
position: absolute;
top: -25px;
left: -8px;
height: 26px;
font-size: 0.9em;
text-align: left;
z-index: 10;
color: #aaa;
text-align: center;
.option {
width: 28px;
display: inline-block;
&.touch {
display: none;
}
&:hover {
color: $--color-primary;
}
}
}
&.is-touch-device .sticky-options {
left: -25px;
width: 150px;
.option.touch {
display: initial;
}
}
}
}
.select-sticky-background {
display: block;
position: absolute;
background-color: hsla(var(--color-foreground-base-h), var(--color-foreground-base-s), var(--color-foreground-base-l), 60%);
border-radius: var(--border-radius-xlarge);
overflow: hidden;
height: calc(100% + 16px);
width: calc(100% + 16px);
left: -8px;
top: -8px;
z-index: 0;
}
</style>

View file

@ -59,3 +59,11 @@ export function filterTemplateNodes(nodes: ITemplatesNode[]) {
export function setPageTitle(title: string) {
window.document.title = title;
}
export function isString(value: unknown): value is string {
return typeof value === 'string';
}
export function isNumber(value: unknown): value is number {
return typeof value === 'number';
}

View file

@ -190,7 +190,6 @@ export const mouseSelect = mixins(
this.$store.commit('resetSelectedNodes');
this.$store.commit('setLastSelectedNode', null);
this.$store.commit('setLastSelectedNodeOutputIndex', null);
this.$store.commit('setActiveNode', null);
// @ts-ignore
this.lastSelectedConnection = null;
// @ts-ignore

View file

@ -4,7 +4,7 @@ import mixins from 'vue-typed-mixins';
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
import { nodeIndex } from '@/components/mixins/nodeIndex';
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE } from '@/constants';
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
import * as CanvasHelpers from '@/views/canvasHelpers';
import { Endpoint } from 'jsplumb';
@ -221,7 +221,15 @@ export const nodeBase = mixins(
// @ts-ignore
this.dragging = true;
if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) {
const isSelected = this.$store.getters.isNodeSelected(this.data.name);
const nodeName = this.data.name;
if (this.data.type === STICKY_NODE_TYPE && !isSelected) {
setTimeout(() => {
this.$emit('nodeSelected', nodeName, false, true);
}, 0);
}
if (params.e && !isSelected) {
// Only the node which gets dragged directly gets an event, for all others it is
// undefined. So check if the currently dragged node is selected and if not clear
// the drag-selection.

View file

@ -65,6 +65,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
export const NOTION_TRIGGER_NODE_TYPE = 'n8n-nodes-base.notionTrigger';
export const PAGERDUTY_NODE_TYPE = 'n8n-nodes-base.pagerDuty';
export const SALESFORCE_NODE_TYPE = 'n8n-nodes-base.salesforce';

View file

@ -65,6 +65,7 @@ import {
N8nRadioButtons,
N8nSelect,
N8nSpinner,
N8nSticky,
N8nTabs,
N8nFormInputs,
N8nFormBox,
@ -100,6 +101,7 @@ Vue.use(N8nMenuItem);
Vue.use(N8nOption);
Vue.use(N8nSelect);
Vue.use(N8nSpinner);
Vue.component('n8n-sticky', N8nSticky);
Vue.use(N8nRadioButtons);
Vue.component('n8n-square-button', N8nSquareButton);
Vue.use(N8nTags);

View file

@ -411,6 +411,7 @@
"nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)",
"nodeSettings.waitBetweenTries.displayName": "Wait Between Tries (ms)",
"nodeView.addNode": "Add node",
"nodeView.addSticky": "Click to add sticky note",
"nodeView.confirmMessage.beforeRouteLeave.cancelButtonText": "Leave without saving",
"nodeView.confirmMessage.beforeRouteLeave.confirmButtonText": "Save",
"nodeView.confirmMessage.beforeRouteLeave.headline": "Save changes before leaving?",

View file

@ -93,7 +93,11 @@ import {
faUserCircle,
faUserFriends,
faUsers,
faStickyNote as faSolidStickyNote,
} from '@fortawesome/free-solid-svg-icons';
import {
faStickyNote,
} from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
function addIcon(icon: any) { // tslint:disable-line:no-any
@ -177,6 +181,8 @@ addIcon(faServer);
addIcon(faSignInAlt);
addIcon(faSlidersH);
addIcon(faSpinner);
addIcon(faSolidStickyNote);
addIcon(faStickyNote);
addIcon(faStop);
addIcon(faSun);
addIcon(faSync);

View file

@ -169,6 +169,9 @@ class Telemetry {
case 'nodeView.addNodeButton':
this.telemetry.track('User added node to workflow canvas', properties);
break;
case 'nodeView.addSticky':
this.telemetry.track('User inserted workflow note', properties);
break;
default:
break;
}

View file

@ -1,5 +1,9 @@
<template>
<div class="node-view-root">
<div
class="node-view-root"
@dragover="onDragOver"
@drop="onDrop"
>
<div
class="node-view-wrapper"
:class="workflowClasses"
@ -11,10 +15,15 @@
@mouseup="mouseUp"
@wheel="wheelScroll"
>
<div id="node-view-background" class="node-view-background" :style="backgroundStyle"></div>
<div id="node-view" class="node-view" :style="workflowStyle">
<div id="node-view-background" class="node-view-background" :style="backgroundStyle" />
<div
id="node-view"
class="node-view"
:style="workflowStyle"
>
<div v-for="nodeData in nodes" :key="getNodeIndex(nodeData.name)">
<node
v-for="nodeData in nodes"
v-if="nodeData.type !== STICKY_NODE_TYPE"
@duplicateNode="duplicateNode"
@deselectAllNodes="deselectAllNodes"
@deselectNode="nodeDeselectedByName"
@ -30,18 +39,42 @@
:instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name"
:hideActions="pullConnActive"
></node>
/>
<Sticky
v-else
@deselectAllNodes="deselectAllNodes"
@deselectNode="nodeDeselectedByName"
@nodeSelected="nodeSelectedByName"
@removeNode="removeNode"
:id="'node-' + getNodeIndex(nodeData.name)"
:name="nodeData.name"
:isReadOnly="isReadOnly"
:instance="instance"
:isActive="!!activeNode && activeNode.name === nodeData.name"
:nodeViewScale="nodeViewScale"
:gridSize="GRID_SIZE"
:hideActions="pullConnActive"
/>
</div>
</div>
</div>
<DataDisplay :renaming="renamingActive" @valueChanged="valueChanged"/>
<div v-if="!createNodeActive && !isReadOnly" class="node-creator-button" :title="$locale.baseText('nodeView.addNode')" @click="() => openNodeCreator('add_node_button')">
<n8n-icon-button size="xlarge" icon="plus" />
<div
class="node-buttons-wrapper"
v-if="!createNodeActive && !isReadOnly"
>
<div class="node-creator-button">
<n8n-icon-button size="xlarge" icon="plus" @click="() => openNodeCreator('add_node_button')" :title="$locale.baseText('nodeView.addNode')"/>
<div class="add-sticky-button" @click="nodeTypeSelected(STICKY_NODE_TYPE)">
<n8n-icon-button size="large" :icon="['far', 'note-sticky']" type="outline" :title="$locale.baseText('nodeView.addSticky')"/>
</div>
</div>
</div>
<node-creator
:active="createNodeActive"
@nodeTypeSelected="nodeTypeSelected"
@closeNodeCreator="closeNodeCreator"
></node-creator>
/>
<div :class="{ 'zoom-menu': true, 'regular-zoom-menu': !isDemo, 'demo-zoom-menu': isDemo, expanded: !sidebarMenuCollapsed }">
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
<font-awesome-icon icon="expand"/>
@ -114,7 +147,7 @@ import {
} from 'jsplumb';
import { MessageBoxInputData } from 'element-ui/types/message-box';
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, STICKY_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
import { copyPaste } from '@/components/mixins/copyPaste';
import { externalHooks } from '@/components/mixins/externalHooks';
import { genericHelpers } from '@/components/mixins/genericHelpers';
@ -133,6 +166,7 @@ import Node from '@/components/Node.vue';
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
import NodeSettings from '@/components/NodeSettings.vue';
import RunData from '@/components/RunData.vue';
import Sticky from '@/components/Sticky.vue';
import * as CanvasHelpers from './canvasHelpers';
@ -177,6 +211,11 @@ import {
import '../plugins/N8nCustomConnectorType';
import '../plugins/PlusEndpointType';
interface AddNodeOptions {
position?: XYPosition;
dragAndDrop?: boolean;
}
export default mixins(
copyPaste,
externalHooks,
@ -198,6 +237,7 @@ export default mixins(
NodeCreator,
NodeSettings,
RunData,
Sticky,
},
errorCaptured: (err, vm, info) => {
console.error('errorCaptured'); // eslint-disable-line no-console
@ -331,6 +371,8 @@ export default mixins(
},
data () {
return {
GRID_SIZE: CanvasHelpers.GRID_SIZE,
STICKY_NODE_TYPE,
createNodeActive: false,
instance: jsPlumb.getInstance(),
lastSelectedConnection: null as null | Connection,
@ -697,7 +739,7 @@ export default mixins(
this.ctrlKeyPressed = true;
} else if (e.key === 'F2' && !this.isReadOnly) {
const lastSelectedNode = this.lastSelectedNode;
if (lastSelectedNode !== null) {
if (lastSelectedNode !== null && lastSelectedNode.type !== STICKY_NODE_TYPE) {
this.callDebounced('renameNodePrompt', { debounceTime: 1500 }, lastSelectedNode.name);
}
} else if ((e.key === '=' || e.key === '+') && !this.isCtrlKeyPressed(e)) {
@ -764,6 +806,9 @@ export default mixins(
const lastSelectedNode = this.lastSelectedNode;
if (lastSelectedNode !== null) {
if (lastSelectedNode.type === STICKY_NODE_TYPE && this.isReadOnly) {
return;
}
this.$store.commit('setActiveNode', lastSelectedNode.name);
}
} else if (e.key === 'ArrowRight' && e.shiftKey === true) {
@ -1227,6 +1272,27 @@ export default mixins(
this.createNodeActive = false;
},
onDragOver(event: DragEvent) {
event.preventDefault();
},
onDrop(event: DragEvent) {
if (!event.dataTransfer) {
return;
}
const nodeTypeName = event.dataTransfer.getData('nodeTypeName');
if (nodeTypeName) {
const mousePosition = this.getMousePositionWithinNodeView(event);
this.addNodeButton(nodeTypeName, {
position: [mousePosition[0] - CanvasHelpers.NODE_SIZE / 2, mousePosition[1] - CanvasHelpers.NODE_SIZE / 2],
dragAndDrop: true,
});
this.createNodeActive = false;
}
},
nodeDeselectedByName (nodeName: string) {
const node = this.$store.getters.getNodeByName(nodeName);
if (node) {
@ -1267,7 +1333,7 @@ export default mixins(
duration: 0,
});
},
async injectNode (nodeTypeName: string) {
async injectNode (nodeTypeName: string, options: AddNodeOptions = {}) {
const nodeTypeData: INodeTypeDescription | null = this.$store.getters.nodeType(nodeTypeName);
if (nodeTypeData === null) {
@ -1297,7 +1363,10 @@ export default mixins(
// when pulling new connection from node or injecting into a connection
const lastSelectedNode = this.lastSelectedNode;
if (lastSelectedNode) {
if (options.position) {
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, options.position);
} else if (lastSelectedNode) {
const lastSelectedConnection = this.lastSelectedConnection;
if (lastSelectedConnection) { // set when injecting into a connection
const [diffX] = CanvasHelpers.getConnectorLengths(lastSelectedConnection);
@ -1308,10 +1377,12 @@ export default mixins(
// set when pulling connections
if (this.newNodeInsertPosition) {
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, [this.newNodeInsertPosition[0] + CanvasHelpers.GRID_SIZE, this.newNodeInsertPosition[1] - CanvasHelpers.NODE_SIZE / 2]);
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, [
this.newNodeInsertPosition[0] + CanvasHelpers.GRID_SIZE,
this.newNodeInsertPosition[1] - CanvasHelpers.NODE_SIZE / 2,
]);
this.newNodeInsertPosition = null;
}
else {
} else {
let yOffset = 0;
if (lastSelectedConnection) {
@ -1352,14 +1423,22 @@ export default mixins(
this.$store.commit('setStateDirty', true);
if (nodeTypeName === STICKY_NODE_TYPE) {
this.$telemetry.trackNodesPanel('nodeView.addSticky', { workflow_id: this.$store.getters.workflowId });
} else {
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', { node_type: nodeTypeName, workflow_id: this.$store.getters.workflowId });
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', {
node_type: nodeTypeName,
workflow_id: this.$store.getters.workflowId,
drag_and_drop: options.dragAndDrop,
} as IDataObject);
}
// Automatically deselect all nodes and select the current one and also active
// current node
this.deselectAllNodes();
setTimeout(() => {
this.nodeSelectedByName(newNodeData.name, true);
this.nodeSelectedByName(newNodeData.name, nodeTypeName !== STICKY_NODE_TYPE);
});
return newNodeData;
@ -1396,7 +1475,7 @@ export default mixins(
this.__addConnection(connectionData, true);
},
async addNodeButton (nodeTypeName: string) {
async addNodeButton (nodeTypeName: string, options: AddNodeOptions = {}) {
if (this.editAllowedCheck() === false) {
return;
}
@ -1405,7 +1484,7 @@ export default mixins(
const lastSelectedNode = this.lastSelectedNode;
const lastSelectedNodeOutputIndex = this.$store.getters.lastSelectedNodeOutputIndex;
const newNodeData = await this.injectNode(nodeTypeName);
const newNodeData = await this.injectNode(nodeTypeName, options);
if (!newNodeData) {
return;
}
@ -2131,8 +2210,12 @@ export default mixins(
}
}
if(node.type === STICKY_NODE_TYPE) {
this.$telemetry.track('User deleted workflow note', { workflow_id: this.$store.getters.workflowId });
} else {
this.$externalHooks().run('node.deleteNode', { node });
this.$telemetry.track('User deleted node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
}
let waitForNewConnection = false;
// connect nodes before/after deleted node
@ -2826,6 +2909,28 @@ export default mixins(
bottom: 10px;
}
.node-buttons-wrapper {
position: fixed;
width: 150px;
height: 200px;
top: 0;
right: 0;
display: flex;
.add-sticky-button {
margin-top: var(--spacing-2xs);
opacity: 0;
transition: .1s;
transition-timing-function: linear;
}
&:hover {
.add-sticky-button {
opacity: 1;
}
}
}
.node-creator-button {
position: fixed;
text-align: center;

View file

@ -1,5 +1,5 @@
import { getStyleTokenValue } from "@/components/helpers";
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE } from "@/constants";
import { getStyleTokenValue, isNumber } from "@/components/helpers";
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE } from "@/constants";
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
import {
@ -217,17 +217,22 @@ export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
return nodes.reduce((accu: IBounds, node: INodeUi) => {
if (node.position[0] < accu.minX) {
accu.minX = node.position[0];
const xOffset = node.type === STICKY_NODE_TYPE && isNumber(node.parameters.width) ? node.parameters.width : NODE_SIZE;
const yOffset = node.type === STICKY_NODE_TYPE && isNumber(node.parameters.height) ? node.parameters.height : NODE_SIZE;
const x = node.position[0];
const y = node.position[1];
if (x < accu.minX) {
accu.minX = x;
}
if (node.position[1] < accu.minY) {
accu.minY = node.position[1];
if (y < accu.minY) {
accu.minY = y;
}
if (node.position[0] > accu.maxX) {
accu.maxX = node.position[0];
if ((x + xOffset) > accu.maxX) {
accu.maxX = x + xOffset;
}
if (node.position[1] > accu.maxY) {
accu.maxY = node.position[1];
if ((y + yOffset) > accu.maxY) {
accu.maxY = y + yOffset;
}
return accu;
@ -592,6 +597,7 @@ export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {off
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
const sidebarWidth = addComponentPadding? SIDEBAR_WIDTH: 0;
const headerHeight = addComponentPadding? HEADER_HEIGHT: 0;
const footerHeight = addComponentPadding? 200: 100;
const PADDING = NODE_SIZE * 4;
@ -605,10 +611,10 @@ export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {off
const zoomLevel = Math.min(scaleX, scaleY, 1);
let xOffset = (minX * -1) * zoomLevel + sidebarWidth; // find top right corner
xOffset += (editorWidth - sidebarWidth - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
xOffset += (editorWidth - sidebarWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
let yOffset = (minY * -1) * zoomLevel + headerHeight; // find top right corner
yOffset += (editorHeight - headerHeight - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
yOffset += (editorHeight - headerHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
return {
zoomLevel,

View file

@ -1,6 +1,6 @@
{
"name": "n8n-node-dev",
"version": "0.52.0",
"version": "0.54.0",
"description": "CLI to simplify n8n credentials/node development",
"license": "SEE LICENSE IN LICENSE.md",
"homepage": "https://n8n.io",
@ -61,8 +61,8 @@
"change-case": "^4.1.1",
"copyfiles": "^2.1.1",
"inquirer": "^7.0.1",
"n8n-core": "~0.113.0",
"n8n-workflow": "~0.95.0",
"n8n-core": "~0.115.0",
"n8n-workflow": "~0.97.0",
"oauth-1.0a": "^2.2.6",
"replace-in-file": "^6.0.0",
"request": "^2.88.2",

View file

@ -31,6 +31,29 @@ export class Aws implements ICredentialType {
password: true,
},
},
{
displayName: 'Temporary Security Credentials',
name: 'temporaryCredentials',
description: 'Support for temporary credentials from AWS STS',
type: 'boolean',
default: false,
},
{
displayName: 'Session Token',
name: 'sessionToken',
type: 'string',
displayOptions: {
show: {
temporaryCredentials: [
true,
],
},
},
default: '',
typeOptions: {
password: true,
},
},
{
displayName: 'Custom Endpoints',
name: 'customEndpoints',

View file

@ -1,9 +1,11 @@
import {
IAuthenticateBearer,
IAuthenticateQueryAuth,
ICredentialTestRequest,
ICredentialType,
INodeProperties,
} from 'n8n-workflow';
export class SlackApi implements ICredentialType {
name = 'slackApi';
displayName = 'Slack API';
@ -17,4 +19,26 @@ export class SlackApi implements ICredentialType {
required: true,
},
];
authenticate: IAuthenticateBearer = {
type: 'bearer',
properties: {
tokenPropertyName: 'accessToken',
},
};
test: ICredentialTestRequest = {
request: {
baseURL: 'https://slack.com',
url: '/api/users.profile.get',
},
rules: [
{
type: 'responseSuccessBody',
properties: {
key: 'error',
value: 'invalid_auth',
message: 'Invalid access token',
},
},
],
};
}

View file

@ -128,7 +128,7 @@ const postalAddressesFields: INodeProperties[] = [
displayName: 'Location',
name: 'location',
type: 'fixedCollection',
default: '',
default: {},
options: [
{
displayName: 'Location Fields',

View file

@ -56,7 +56,6 @@ export const accountContactFields: INodeProperties[] = [
],
},
},
description: 'Account ID',
},
{
displayName: 'Contact ID',
@ -74,7 +73,6 @@ export const accountContactFields: INodeProperties[] = [
],
},
},
description: 'Contact ID',
},
{
displayName: 'Additional Fields',

View file

@ -51,7 +51,6 @@ export const contactListFields: INodeProperties[] = [
],
},
},
description: 'List ID',
},
{
displayName: 'Contact ID',
@ -69,7 +68,6 @@ export const contactListFields: INodeProperties[] = [
],
},
},
description: 'Contact ID',
},
// ----------------------------------
@ -91,7 +89,6 @@ export const contactListFields: INodeProperties[] = [
],
},
},
description: 'List ID',
},
{
displayName: 'Contact ID',
@ -109,6 +106,5 @@ export const contactListFields: INodeProperties[] = [
],
},
},
description: 'Contact ID',
},
];

View file

@ -54,7 +54,6 @@ export const contactTagFields: INodeProperties[] = [
],
},
},
description: 'Tag ID',
},
{
displayName: 'Contact ID',
@ -72,7 +71,6 @@ export const contactTagFields: INodeProperties[] = [
],
},
},
description: 'Contact ID',
},
// ----------------------------------
// contactTag:delete

View file

@ -73,7 +73,7 @@ export const ecomOrderFields: INodeProperties[] = [
],
},
},
description: 'The id of the order in the external service. ONLY REQUIRED IF EXTERNALCHECKOUTID NOT INCLUDED',
description: 'The id of the order in the external service. ONLY REQUIRED IF EXTERNALCHECKOUTID NOT INCLUDED.',
},
{
displayName: 'External checkout ID',
@ -437,7 +437,7 @@ export const ecomOrderFields: INodeProperties[] = [
name: 'externalid',
type: 'string',
default: '',
description: 'The id of the order in the external service. ONLY REQUIRED IF EXTERNALCHECKOUTID NOT INCLUDED',
description: 'The id of the order in the external service. ONLY REQUIRED IF EXTERNALCHECKOUTID NOT INCLUDED.',
},
{
displayName: 'External checkout ID',

View file

@ -155,7 +155,7 @@ export class Affinity implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = items.length;
let responseData;
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -183,6 +183,7 @@ export const companyFields: INodeProperties[] = [
],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-simplify
default: false,
description: 'Return a simplified version of the response instead of the raw data.',
},
@ -206,7 +207,7 @@ export const companyFields: INodeProperties[] = [
],
},
},
default: '',
default: {},
placeholder: 'Add Condition',
options: [
{
@ -319,7 +320,6 @@ export const companyFields: INodeProperties[] = [
},
},
default: '',
description: '',
},
{
displayName: 'Options',
@ -388,7 +388,6 @@ export const companyFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -600,7 +599,6 @@ export const companyFields: INodeProperties[] = [
name: 'customProperties',
type: 'fixedCollection',
default: {},
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},
@ -686,7 +684,6 @@ export const companyFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -898,7 +895,6 @@ export const companyFields: INodeProperties[] = [
name: 'customProperties',
type: 'fixedCollection',
default: {},
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},

View file

@ -183,6 +183,7 @@ export const contactFields: INodeProperties[] = [
],
},
},
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-simplify
default: false,
description: 'Return a simplified version of the response instead of the raw data.',
},
@ -206,7 +207,7 @@ export const contactFields: INodeProperties[] = [
],
},
},
default: '',
default: {},
placeholder: 'Add Condition',
options: [
{
@ -319,7 +320,6 @@ export const contactFields: INodeProperties[] = [
},
},
default: '',
description: '',
},
{
displayName: 'Options',
@ -389,7 +389,6 @@ export const contactFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -539,7 +538,6 @@ export const contactFields: INodeProperties[] = [
type: 'string',
required: true,
default: '',
description: 'Email',
},
],
},
@ -769,7 +767,6 @@ export const contactFields: INodeProperties[] = [
name: 'customProperties',
type: 'fixedCollection',
default: {},
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},
@ -855,7 +852,6 @@ export const contactFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -1003,7 +999,6 @@ export const contactFields: INodeProperties[] = [
type: 'string',
required: true,
default: '',
description: 'Email',
},
],
},
@ -1233,7 +1228,6 @@ export const contactFields: INodeProperties[] = [
name: 'customProperties',
type: 'fixedCollection',
default: {},
description: 'Custom Properties',
typeOptions: {
multipleValues: true,
},

View file

@ -231,7 +231,6 @@ export const dealFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -302,7 +301,6 @@ export const dealFields: INodeProperties[] = [
name: 'customData',
type: 'fixedCollection',
default: {},
description: 'Custom Data',
typeOptions: {
multipleValues: true,
},
@ -381,7 +379,6 @@ export const dealFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -482,7 +479,6 @@ export const dealFields: INodeProperties[] = [
name: 'customData',
type: 'fixedCollection',
default: {},
description: 'Custom Data',
typeOptions: {
multipleValues: true,
},

View file

@ -433,7 +433,7 @@ export class Airtable implements INodeType {
},
},
default: '',
description: 'Comma separated list of fields to ignore.',
description: 'Comma-separated list of fields to ignore.',
},
{
displayName: 'Typecast',

View file

@ -137,7 +137,7 @@ export class Automizy implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const length = items.length;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -110,7 +110,7 @@ export const contactFields: INodeProperties[] = [
displayName: 'Custom Fields',
name: 'customFieldsUi',
type: 'fixedCollection',
default: '',
default: {},
placeholder: 'Add Custom Field',
typeOptions: {
multipleValues: true,
@ -394,7 +394,7 @@ export const contactFields: INodeProperties[] = [
displayName: 'Custom Fields',
name: 'customFieldsUi',
type: 'fixedCollection',
default: '',
default: {},
placeholder: 'Add Custom Field',
typeOptions: {
multipleValues: true,

View file

@ -153,7 +153,7 @@ export class Autopilot implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = (items.length as unknown) as number;
const length = items.length;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -91,7 +91,7 @@ export const contactFields: INodeProperties[] = [
displayName: 'Custom Fields',
name: 'customFieldsUi',
type: 'fixedCollection',
default: '',
default: {},
placeholder: 'Add Custom Field',
typeOptions: {
multipleValues: true,

View file

@ -46,8 +46,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -40,7 +40,11 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Concatenate path and instantiate URL object so it parses correctly query strings
const endpoint = new URL(getEndpointForService(service, credentials) + path);
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
const options = sign({
// @ts-ignore
uri: endpoint,
@ -50,10 +54,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
path: '/',
headers: { ...headers },
body: JSON.stringify(body),
}, {
accessKeyId: credentials.accessKeyId,
secretAccessKey: credentials.secretAccessKey,
});
}, securityHeaders);
try {
return JSON.parse(await this.helpers.request!(options));

View file

@ -172,10 +172,10 @@ export const itemFields: INodeProperties[] = [
{
displayName: 'Expression Attribute Values',
name: 'eavUi',
description: 'Substitution tokens for attribute names in an expression. Only needed when the parameter "condition expression" is set',
description: 'Substitution tokens for attribute names in an expression. Only needed when the parameter "condition expression" is set.',
placeholder: 'Add Attribute Value',
type: 'fixedCollection',
default: '',
default: {},
required: true,
typeOptions: {
multipleValues: true,
@ -223,14 +223,14 @@ export const itemFields: INodeProperties[] = [
name: 'conditionExpression',
type: 'string',
default: '',
description: 'A condition that must be satisfied in order for a conditional upsert to succeed. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>',
description: 'A condition that must be satisfied in order for a conditional upsert to succeed. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>.',
},
{
displayName: 'Expression Attribute Names',
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: '',
default: {},
typeOptions: {
multipleValues: true,
},
@ -254,7 +254,7 @@ export const itemFields: INodeProperties[] = [
],
},
],
description: 'One or more substitution tokens for attribute names in an expression. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>',
description: 'One or more substitution tokens for attribute names in an expression. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>.',
},
],
},
@ -401,7 +401,7 @@ export const itemFields: INodeProperties[] = [
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: '',
default: {},
typeOptions: {
multipleValues: true,
},
@ -425,15 +425,15 @@ export const itemFields: INodeProperties[] = [
],
},
],
description: 'One or more substitution tokens for attribute names in an expression. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>',
description: 'One or more substitution tokens for attribute names in an expression. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>.',
},
{
displayName: 'Expression Attribute Values',
name: 'expressionAttributeUi',
description: 'Substitution tokens for attribute names in an expression. Only needed when the parameter "condition expression" is set',
description: 'Substitution tokens for attribute names in an expression. Only needed when the parameter "condition expression" is set.',
placeholder: 'Add Attribute Value',
type: 'fixedCollection',
default: '',
default: {},
required: true,
typeOptions: {
multipleValues: true,
@ -624,7 +624,7 @@ export const itemFields: INodeProperties[] = [
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: '',
default: {},
typeOptions: {
multipleValues: true,
},
@ -648,7 +648,7 @@ export const itemFields: INodeProperties[] = [
],
},
],
description: 'One or more substitution tokens for attribute names in an expression. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>',
description: 'One or more substitution tokens for attribute names in an expression. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">View details</a>.',
},
{
displayName: 'Read Type',
@ -665,7 +665,7 @@ export const itemFields: INodeProperties[] = [
},
],
default: 'eventuallyConsistentRead',
description: 'Type of read to perform on the table. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html">View details</a>',
description: 'Type of read to perform on the table. <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html">View details</a>.',
},
],
},
@ -688,7 +688,7 @@ export const itemFields: INodeProperties[] = [
},
},
default: false,
description: 'Whether to do an scan or query. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html" >differences</a>',
description: 'Whether to do an scan or query. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-query-scan.html" >differences</a>.',
},
{
displayName: 'Filter Expression',
@ -733,7 +733,7 @@ export const itemFields: INodeProperties[] = [
description: 'Substitution tokens for attribute names in an expression',
placeholder: 'Add Attribute Value',
type: 'fixedCollection',
default: '',
default: {},
required: true,
typeOptions: {
multipleValues: true,
@ -924,14 +924,14 @@ export const itemFields: INodeProperties[] = [
},
},
default: '',
description: 'Text that contains conditions that DynamoDB applies after the Query operation, but before the data is returned. Items that do not satisfy the FilterExpression criteria are not returned',
description: 'Text that contains conditions that DynamoDB applies after the Query operation, but before the data is returned. Items that do not satisfy the FilterExpression criteria are not returned.',
},
{
displayName: 'Expression Attribute Names',
name: 'eanUi',
placeholder: 'Add Expression',
type: 'fixedCollection',
default: '',
default: {},
typeOptions: {
multipleValues: true,
},
@ -955,7 +955,7 @@ export const itemFields: INodeProperties[] = [
],
},
],
description: 'One or more substitution tokens for attribute names in an expression. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>',
description: 'One or more substitution tokens for attribute names in an expression. Check <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html">Info</a>.',
},
],
},

View file

@ -36,8 +36,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -205,7 +205,7 @@ export class AwsRekognition implements INodeType {
displayName: 'Regions of Interest',
name: 'regionsOfInterestUi',
type: 'fixedCollection',
default: '',
default: {},
placeholder: 'Add Region of Interest',
displayOptions: {
show: {
@ -272,7 +272,7 @@ export class AwsRekognition implements INodeType {
displayName: 'Word Filter',
name: 'wordFilterUi',
type: 'collection',
default: '',
default: {},
placeholder: 'Add Word Filter',
displayOptions: {
show: {

View file

@ -43,8 +43,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = {headers: headers || {}, host: endpoint.host, method, path, body} as Request;
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()});
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -22,6 +22,7 @@ import {
INodeExecutionData,
INodeType,
INodeTypeDescription,
JsonObject,
NodeOperationError,
} from 'n8n-workflow';
@ -632,7 +633,7 @@ export class AwsS3 implements INodeType {
}
} catch (error) {
if (this.continueOnFail()) {
returnData.push({ error: error.message });
returnData.push({ error: (error as JsonObject).message });
continue;
}
throw error;

View file

@ -348,7 +348,7 @@ export const bucketFields: INodeProperties[] = [
name: 'startAfter',
type: 'string',
default: '',
description: 'StartAfter is where you want Amazon S3 to start listing from. Amazon S3 starts listing after this specified key',
description: 'StartAfter is where you want Amazon S3 to start listing from. Amazon S3 starts listing after this specified key.',
},
],
},

View file

@ -671,7 +671,7 @@ export const fileFields: INodeProperties[] = [
name: 'tagsUi',
placeholder: 'Add Tag',
type: 'fixedCollection',
default: '',
default: {},
typeOptions: {
multipleValues: true,
},
@ -695,14 +695,12 @@ export const fileFields: INodeProperties[] = [
name: 'key',
type: 'string',
default: '',
description: '',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: '',
},
],
},

View file

@ -27,7 +27,7 @@ import {
} from 'n8n-core';
import {
IDataObject, NodeApiError, NodeOperationError,
IDataObject, JsonObject, NodeApiError, NodeOperationError,
} from 'n8n-workflow';
export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions | IWebhookFunctions, service: string, method: string, path: string, body?: string | Buffer, query: IDataObject = {}, headers?: object, option: IDataObject = {}, region?: string): Promise<any> { // tslint:disable-line:no-any
@ -37,9 +37,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = {headers: headers || {}, host: endpoint.host, method, path: `${endpoint.pathname}?${queryToString(query).replace(/\+/g, '%2B')}`, body} as Request;
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim()});
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,
@ -55,7 +59,7 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
try {
return await this.helpers.request!(options);
} catch (error) {
throw new NodeApiError(this.getNode(), error);
throw new NodeApiError(this.getNode(), (error as JsonObject));
}
}

View file

@ -38,7 +38,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -55,7 +55,6 @@ export class AwsTextract implements INodeType {
},
],
default: 'analyzeExpense',
description: '',
},
{
displayName: 'Input Data Field Name',
@ -70,7 +69,7 @@ export class AwsTextract implements INodeType {
},
},
required: true,
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG',
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG.',
},
{
displayName: 'Simplify Response',

View file

@ -49,8 +49,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,
@ -131,7 +136,13 @@ export async function validateCrendetials(this: ICredentialTestFunctions, decryp
// Sign AWS API request with the user credentials
const signOpts = { host: endpoint.host, method: 'POST', path: '?Action=GetCallerIdentity&Version=2011-06-15' } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -49,8 +49,13 @@ export async function awsApiRequest(this: IHookFunctions | IExecuteFunctions | I
// Sign AWS API request with the user credentials
const signOpts = { headers: headers || {}, host: endpoint.host, method, path, body } as Request;
sign(signOpts, { accessKeyId: `${credentials.accessKeyId}`.trim(), secretAccessKey: `${credentials.secretAccessKey}`.trim() });
const securityHeaders = {
accessKeyId: `${credentials.accessKeyId}`.trim(),
secretAccessKey: `${credentials.secretAccessKey}`.trim(),
sessionToken: credentials.temporaryCredentials ? `${credentials.sessionToken}`.trim() : undefined,
};
sign(signOpts, securityHeaders);
const options: OptionsWithUri = {
headers: signOpts.headers,

View file

@ -28,7 +28,6 @@ export const descriptions: INodeProperties[] = [
},
],
default: 'get',
description: '',
},
...get.description,
];

View file

@ -50,7 +50,7 @@ export const createEmployeeSharedDescription = (sync = false): INodeProperties[]
type: 'string',
default: '',
placeholder: 'United States',
description: 'The name of the country. Must exist in the BambooHr country list',
description: 'The name of the country. Must exist in the BambooHr country list.',
},
],
},

View file

@ -49,7 +49,6 @@ export const descriptions: INodeProperties[] = [
},
],
default: 'create',
description: '',
},
...create.description,
...get.description,

View file

@ -50,7 +50,7 @@ export const updateEmployeeSharedDescription = (sync = false): INodeProperties[]
type: 'string',
default: '',
placeholder: 'United States',
description: 'The name of the country. Must exist in the BambooHr country list',
description: 'The name of the country. Must exist in the BambooHr country list.',
},
],
},

View file

@ -54,7 +54,7 @@ export const employeeDocumentUploadDescription: EmployeeDocumentProperties = [
},
},
required: true,
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG',
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG.',
},
{
displayName: 'Options',

View file

@ -56,7 +56,6 @@ export const descriptions: INodeProperties[] = [
},
],
default: 'delete',
description: '',
},
...del.description,
...download.description,

View file

@ -17,7 +17,7 @@ export const fileUploadDescription: INodeProperties[] = [
},
},
required: true,
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG',
description: 'The name of the input field containing the binary file data to be uploaded. Supported file types: PNG, JPEG.',
},
{
displayName: 'Category Name/ID',

View file

@ -113,7 +113,7 @@ export class Bannerbear implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = items.length;
let responseData;
const qs: IDataObject = {};
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -297,37 +297,37 @@ export const operationFields: INodeProperties[] = [
{
name: 'Date Equal',
value: 'date_equal',
description: 'Field is date. Format: \'YYYY-MM-DD\'',
description: 'Field is date. Format: \'YYYY-MM-DD\'.',
},
{
name: 'Date Not Equal',
value: 'date_not_equal',
description: 'Field is not date. Format: \'YYYY-MM-DD\'',
description: 'Field is not date. Format: \'YYYY-MM-DD\'.',
},
{
name: 'Date Equals Today',
value: 'date_equals_today',
description: 'Field is today. Format: string',
description: 'Field is today. Format: string.',
},
{
name: 'Date Equals Month',
value: 'date_equals_month',
description: 'Field in this month. Format: string',
description: 'Field in this month. Format: string.',
},
{
name: 'Date Equals Year',
value: 'date_equals_year',
description: 'Field in this year. Format: string',
description: 'Field in this year. Format: string.',
},
{
name: 'Date Before Date',
value: 'date_before',
description: 'Field before this date. Format: \'YYYY-MM-DD\'',
description: 'Field before this date. Format: \'YYYY-MM-DD\'.',
},
{
name: 'Date After Date',
value: 'date_after',
description: 'Field after this date. Format: \'YYYY-MM-DD\'',
description: 'Field after this date. Format: \'YYYY-MM-DD\'.',
},
{
name: 'Filename Contains',

View file

@ -174,7 +174,6 @@ export class Beeminder implements INodeType {
name: 'datapointId',
type: 'string',
default: '',
description: 'Datapoint id',
displayOptions: {
show: {
operation: [
@ -207,7 +206,6 @@ export class Beeminder implements INodeType {
name: 'comment',
type: 'string',
default: '',
description: 'Comment',
},
{
displayName: 'Timestamp',
@ -284,7 +282,6 @@ export class Beeminder implements INodeType {
name: 'comment',
type: 'string',
default: '',
description: 'Comment',
},
{
displayName: 'Timestamp',
@ -326,7 +323,7 @@ export class Beeminder implements INodeType {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = items.length;
const timezone = this.getTimezone();
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -134,7 +134,7 @@ export class Bitly implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = items.length;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -80,7 +80,7 @@ export class Box implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = items.length;
const qs: IDataObject = {};
let responseData;
const timezone = this.getTimezone();

View file

@ -77,7 +77,6 @@ export const fileFields: INodeProperties[] = [
},
},
default: '',
description: 'File ID',
},
{
displayName: 'Parent ID',
@ -94,7 +93,7 @@ export const fileFields: INodeProperties[] = [
],
},
},
description: 'The ID of folder to copy the file to. If not defined will be copied to the root folder',
description: 'The ID of folder to copy the file to. If not defined will be copied to the root folder.',
},
{
displayName: 'Additional Fields',
@ -176,7 +175,6 @@ export const fileFields: INodeProperties[] = [
},
},
default: '',
description: 'File ID',
},
{
displayName: 'Binary Property',
@ -327,7 +325,7 @@ export const fileFields: INodeProperties[] = [
name: 'contet_types',
type: 'string',
default: '',
description: `Limits search results to items with the given content types. Content types are defined as a comma separated lists of Box recognized content types.`,
description: 'Limits search results to items with the given content types. Content types are defined as a comma-separated lists of Box recognized content types.',
},
{
displayName: 'Created At Range',
@ -396,7 +394,7 @@ export const fileFields: INodeProperties[] = [
name: 'ancestor_folder_ids',
type: 'string',
default: '',
description: `Limits search results to items within the given list of folders. Folders are defined as a comma separated lists of folder IDs.`,
description: 'Limits search results to items within the given list of folders. Folders are defined as a comma-separated lists of folder IDs.',
},
{
displayName: 'Scope',
@ -421,7 +419,7 @@ export const fileFields: INodeProperties[] = [
type: 'string',
default: '',
placeholder: '1000000,5000000',
description: `Limits search results to items within a given file size range. File size ranges are defined as comma separated byte sizes.`,
description: 'Limits search results to items within a given file size range. File size ranges are defined as comma-separated byte sizes.',
},
{
displayName: 'Sort',
@ -492,7 +490,7 @@ export const fileFields: INodeProperties[] = [
name: 'owner_user_ids',
type: 'string',
default: '',
description: `Limits search results to items owned by the given list of owners. Owners are defined as a comma separated list of user IDs.`,
description: 'Limits search results to items owned by the given list of owners. Owners are defined as a comma-separated list of user IDs.',
},
],
},
@ -830,6 +828,6 @@ export const fileFields: INodeProperties[] = [
},
},
default: '',
description: 'ID of the parent folder that will contain the file. If not it will be uploaded to the root folder',
description: 'ID of the parent folder that will contain the file. If not it will be uploaded to the root folder.',
},
];

View file

@ -89,7 +89,7 @@ export const folderFields: INodeProperties[] = [
},
},
default: '',
description: 'ID of the folder you want to create the new folder in. if not defined it will be created on the root folder',
description: 'ID of the folder you want to create the new folder in. if not defined it will be created on the root folder.',
},
{
displayName: 'Options',
@ -155,7 +155,6 @@ export const folderFields: INodeProperties[] = [
},
},
default: '',
description: 'Folder ID',
},
/* -------------------------------------------------------------------------- */
@ -176,7 +175,6 @@ export const folderFields: INodeProperties[] = [
},
},
default: '',
description: 'Folder ID',
},
{
displayName: 'Recursive',
@ -279,7 +277,7 @@ export const folderFields: INodeProperties[] = [
name: 'contet_types',
type: 'string',
default: '',
description: `Limits search results to items with the given content types. Content types are defined as a comma separated lists of Box recognized content types.`,
description: 'Limits search results to items with the given content types. Content types are defined as a comma-separated lists of Box recognized content types.',
},
{
displayName: 'Created At Range',
@ -348,7 +346,7 @@ export const folderFields: INodeProperties[] = [
name: 'ancestor_folder_ids',
type: 'string',
default: '',
description: `Limits search results to items within the given list of folders. Folders are defined as a comma separated lists of folder IDs.`,
description: 'Limits search results to items within the given list of folders. Folders are defined as a comma-separated lists of folder IDs.',
},
{
displayName: 'Scope',
@ -373,7 +371,7 @@ export const folderFields: INodeProperties[] = [
type: 'string',
default: '',
placeholder: '1000000,5000000',
description: `Limits search results to items within a given file size range. File size ranges are defined as comma separated byte sizes.`,
description: 'Limits search results to items within a given file size range. File size ranges are defined as comma-separated byte sizes.',
},
{
displayName: 'Sort',
@ -444,7 +442,7 @@ export const folderFields: INodeProperties[] = [
name: 'owner_user_ids',
type: 'string',
default: '',
description: `Limits search results to items owned by the given list of owners. Owners are defined as a comma separated list of user IDs.`,
description: 'Limits search results to items owned by the given list of owners. Owners are defined as a comma-separated list of user IDs.',
},
],
},
@ -703,7 +701,6 @@ export const folderFields: INodeProperties[] = [
},
},
default: '',
description: 'Folder ID',
},
{
displayName: 'Update Fields',

View file

@ -161,7 +161,7 @@ export class Brandfetch implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const length = items.length as unknown as number;
const length = items.length;
const operation = this.getNodeParameter('operation', 0) as string;
const responseData = [];

View file

@ -297,7 +297,6 @@ export const objectFields: INodeProperties[] = [
name: 'jsonParameters',
type: 'boolean',
default: false,
description: '',
displayOptions: {
show: {
resource: [
@ -471,7 +470,7 @@ export const objectFields: INodeProperties[] = [
},
},
placeholder: `[ { "key": "name", "constraint_type": "text contains", "value": "cafe" } , { "key": "address", "constraint_type": "geographic_search", "value": { "range":10, "origin_address":"New York" } } ]`,
description: 'Refine the list that is returned by the Data API with search constraints, exactly as you define a search in Bubble. See <a href="https://manual.bubble.io/core-resources/api/data-api#search-constraints">link</a>',
description: 'Refine the list that is returned by the Data API with search constraints, exactly as you define a search in Bubble. See <a href="https://manual.bubble.io/core-resources/api/data-api#search-constraints">link</a>.',
},
{
displayName: 'Sort',
@ -492,7 +491,7 @@ export const objectFields: INodeProperties[] = [
name: 'sort_field',
type: 'string',
default: '',
description: `Specify the field to use for sorting. Either use a fielddefined for the current typeor use <code>_random_sorting</code> to get the entries in a random order`,
description: 'Specify the field to use for sorting. Either use a fielddefined for the current typeor use <code>_random_sorting</code> to get the entries in a random order.',
},
{
displayName: 'Descending',

View file

@ -1 +1 @@
<svg height="2500" viewBox="7.4 0 344.6 360" width="2342" xmlns="http://www.w3.org/2000/svg"><g fill="#676b74"><path d="M313.8 360H45.5c-21 0-38.1-17.1-38.1-38.1V53.5c0-21 17.1-38.1 38.1-38.1h268.3c21 0 38.1 17.1 38.1 38.1v268.3c.1 21.1-17 38.2-38.1 38.2zM45.5 36.5c-9.4 0-17 7.6-17 17v268.3c0 9.4 7.6 17 17 17h268.3c9.4 0 17-7.6 17-17V53.5c0-9.4-7.6-17-17-17z"/><path d="M256.6 72.4c-4.5 0-8.1-3.6-8.1-8.1V8.1c0-4.5 3.6-8.1 8.1-8.1s8.1 3.6 8.1 8.1v56.1c0 4.5-3.6 8.2-8.1 8.2zm-154.7 0c-4.5 0-8.1-3.6-8.1-8.1V8.1c0-4.5 3.6-8.1 8.1-8.1s8.1 3.6 8.1 8.1v56.1c.1 4.5-3.6 8.2-8.1 8.2zm87.5 181.4c-33.6 0-60.9-27.3-60.9-60.9s27.3-60.9 60.9-60.9c15.2 0 29.7 5.6 40.9 15.8 1.4 1.2 1.5 3.4.2 4.7-1.2 1.4-3.4 1.5-4.7.2-10-9.1-22.9-14.1-36.4-14.1-29.9 0-54.2 24.3-54.2 54.2s24.3 54.2 54.2 54.2c13.5 0 26.4-5 36.4-14.1 1.4-1.2 3.5-1.1 4.7.2 1.2 1.4 1.1 3.5-.2 4.7-11.2 10.4-25.7 16-40.9 16z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 525.8 535.73"><defs><style>.cls-1{fill:none;}.cls-2{fill:#006bff;}.cls-3{fill:#0ae9ef;}</style></defs><g id="Layer_2" data-name="Layer 2"><g id="Logo_assets" data-name="Logo assets"><g id="Brand_mark" data-name="Brand mark"><path class="cls-1" d="M443.74,337.62l-27.16,47.05a139.52,139.52,0,0,1-120.82,69.75H241.43a139.52,139.52,0,0,1-120.82-69.75L93.45,337.62a139.52,139.52,0,0,1,0-139.51l27.16-47.05A139.53,139.53,0,0,1,241.43,81.3h54.33a139.53,139.53,0,0,1,120.82,69.76l27.16,47.05a139.23,139.23,0,0,1,8.55,17.55c0,.12.09.23.13.35a102.15,102.15,0,0,0,44.33-18.24c0-.14-.08-.28-.13-.43a237.8,237.8,0,0,0-33.29-67.58,240.67,240.67,0,0,0-52-53.48A239.3,239.3,0,0,0,98.65,437.08a239.43,239.43,0,0,0,398-98.69c.05-.15.09-.29.13-.43a102.15,102.15,0,0,0-44.33-18.24c0,.12-.09.23-.13.35A139.23,139.23,0,0,1,443.74,337.62Z"/><path class="cls-2" d="M360.4,347.4c-17,15.09-38.21,33.87-76.78,33.87h-23c-27.88,0-53.23-10.12-71.37-28.49-17.72-17.94-27.48-42.5-27.48-69.16V252.11c0-26.66,9.76-51.22,27.48-69.16,18.14-18.37,43.49-28.49,71.37-28.49h23c38.57,0,59.76,18.78,76.78,33.87,17.65,15.65,32.9,29.16,73.52,29.16a116.05,116.05,0,0,0,18.5-1.48c0-.12-.08-.23-.13-.35a139.23,139.23,0,0,0-8.55-17.55l-27.16-47.05A139.53,139.53,0,0,0,295.76,81.3H241.43a139.53,139.53,0,0,0-120.82,69.76L93.45,198.11a139.52,139.52,0,0,0,0,139.51l27.16,47.05a139.52,139.52,0,0,0,120.82,69.75h54.33a139.52,139.52,0,0,0,120.82-69.75l27.16-47.05a139.23,139.23,0,0,0,8.55-17.55c0-.12.09-.23.13-.35a116.05,116.05,0,0,0-18.5-1.48C393.3,318.24,378.05,331.75,360.4,347.4Z"/><path class="cls-2" d="M283.62,183h-23c-42.42,0-70.3,30.3-70.3,69.09v31.51c0,38.79,27.88,69.09,70.3,69.09h23c61.82,0,57-63,150.3-63a144.19,144.19,0,0,1,26.37,2.41,139.36,139.36,0,0,0,0-48.46,143.32,143.32,0,0,1-26.37,2.42C340.59,246.05,345.44,183,283.62,183Z"/><path class="cls-2" d="M513.91,315.13a130.21,130.21,0,0,0-53.62-23c0,.16-.05.32-.08.47a138.46,138.46,0,0,1-7.79,27.16A102.15,102.15,0,0,1,496.75,338c0,.14-.08.28-.13.43A237.8,237.8,0,0,1,463.33,406a240.67,240.67,0,0,1-52,53.48A239.3,239.3,0,0,1,98.65,98.65a239.43,239.43,0,0,1,398,98.69c.05.15.09.29.13.43A102.15,102.15,0,0,1,452.42,216a139.36,139.36,0,0,1,7.8,27.18c0,.15,0,.3.07.44a129.94,129.94,0,0,0,53.62-23c15.29-11.31,12.33-24.09,10-31.65C490.22,79.52,388.33,0,267.86,0,119.93,0,0,119.93,0,267.86S119.93,535.73,267.86,535.73c120.47,0,222.36-79.52,256-188.94C526.24,339.23,529.2,326.45,513.91,315.13Z"/><path class="cls-3" d="M452.42,216a116.05,116.05,0,0,1-18.5,1.48c-40.62,0-55.87-13.51-73.52-29.16-17-15.09-38.21-33.87-76.78-33.87h-23c-27.88,0-53.23,10.12-71.37,28.49-17.72,17.94-27.48,42.5-27.48,69.16v31.51c0,26.66,9.76,51.22,27.48,69.16,18.14,18.37,43.49,28.49,71.37,28.49h23c38.57,0,59.76-18.78,76.78-33.87,17.65-15.65,32.9-29.16,73.52-29.16a116.05,116.05,0,0,1,18.5,1.48,138.46,138.46,0,0,0,7.79-27.16c0-.15.06-.31.08-.47a144.19,144.19,0,0,0-26.37-2.41c-93.33,0-88.48,63-150.3,63h-23c-42.42,0-70.3-30.3-70.3-69.09V252.11c0-38.79,27.88-69.09,70.3-69.09h23c61.82,0,57,63,150.3,63a143.32,143.32,0,0,0,26.37-2.42c0-.14,0-.29-.07-.44A139.36,139.36,0,0,0,452.42,216Z"/><path class="cls-3" d="M452.42,216a116.05,116.05,0,0,1-18.5,1.48c-40.62,0-55.87-13.51-73.52-29.16-17-15.09-38.21-33.87-76.78-33.87h-23c-27.88,0-53.23,10.12-71.37,28.49-17.72,17.94-27.48,42.5-27.48,69.16v31.51c0,26.66,9.76,51.22,27.48,69.16,18.14,18.37,43.49,28.49,71.37,28.49h23c38.57,0,59.76-18.78,76.78-33.87,17.65-15.65,32.9-29.16,73.52-29.16a116.05,116.05,0,0,1,18.5,1.48,138.46,138.46,0,0,0,7.79-27.16c0-.15.06-.31.08-.47a144.19,144.19,0,0,0-26.37-2.41c-93.33,0-88.48,63-150.3,63h-23c-42.42,0-70.3-30.3-70.3-69.09V252.11c0-38.79,27.88-69.09,70.3-69.09h23c61.82,0,57,63,150.3,63a143.32,143.32,0,0,0,26.37-2.42c0-.14,0-.29-.07-.44A139.36,139.36,0,0,0,452.42,216Z"/></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 891 B

After

Width:  |  Height:  |  Size: 3.7 KiB

View file

@ -61,7 +61,7 @@ export class CircleCi implements INodeType {
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
const length = items.length;
const qs: IDataObject = {};
let responseData;
const resource = this.getNodeParameter('resource', 0) as string;

View file

@ -164,7 +164,7 @@ export function getActionInheritedProperties(): INodeProperties[] {
name: 'iconUrl',
type: 'string',
default: '',
description: 'Optional icon to be shown on the action in conjunction with the title. Supports data URI in version 1.2+',
description: 'Optional icon to be shown on the action in conjunction with the title. Supports data URI in version 1.2+.',
},
{
displayName: 'Style',
@ -205,7 +205,7 @@ export function getTextBlockProperties(): INodeProperties[] {
},
},
required: true,
description: 'Text to display. A subset of markdown is supported (https://aka.ms/ACTextFeatures)',
description: 'Text to display. A subset of markdown is supported (https://aka.ms/ACTextFeatures).',
},
{
displayName: 'Color',
@ -407,7 +407,7 @@ export function getTextBlockProperties(): INodeProperties[] {
},
},
default: true,
description: 'If true, allow text to wrap. Otherwise, text is clipped',
description: 'If true, allow text to wrap. Otherwise, text is clipped.',
},
{
displayName: 'Height',
@ -537,7 +537,7 @@ export function getInputTextProperties(): INodeProperties[] {
},
},
default: '',
description: 'Unique identifier for the value. Used to identify collected input when the Submit action is performed',
description: 'Unique identifier for the value. Used to identify collected input when the Submit action is performed.',
},
{
displayName: 'Is Multiline',
@ -579,7 +579,7 @@ export function getInputTextProperties(): INodeProperties[] {
},
},
default: '',
description: 'Description of the input desired. Displayed when no text has been input',
description: 'Description of the input desired. Displayed when no text has been input.',
},
{
displayName: 'Regex',

Some files were not shown because too many files have changed in this diff Show more