mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 04:47:29 -08:00
🔀 Merge master
This commit is contained in:
commit
121898dd45
1451
package-lock.json
generated
1451
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -26,7 +26,7 @@ var nodeVersion = process.versions.node.split('.');
|
||||||
|
|
||||||
if (parseInt(nodeVersion[0], 10) < 14) {
|
if (parseInt(nodeVersion[0], 10) < 14) {
|
||||||
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 14 or later!\n`);
|
console.log(`\nYour Node.js version (${process.versions.node}) is too old to run n8n.\nPlease update to version 14 or later!\n`);
|
||||||
process.exit(0);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
require('@oclif/command').run()
|
require('@oclif/command').run()
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
|
GenericHelpers,
|
||||||
InternalHooksManager,
|
InternalHooksManager,
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
|
@ -125,7 +126,8 @@ export class Execute extends Command {
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
InternalHooksManager.init(instanceId);
|
const { cli } = await GenericHelpers.getVersions();
|
||||||
|
InternalHooksManager.init(instanceId, cli);
|
||||||
|
|
||||||
// Add the found types to an instance other parts of the application can use
|
// Add the found types to an instance other parts of the application can use
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
|
|
@ -28,6 +28,7 @@ import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
|
GenericHelpers,
|
||||||
InternalHooksManager,
|
InternalHooksManager,
|
||||||
IWorkflowDb,
|
IWorkflowDb,
|
||||||
IWorkflowExecutionDataProcess,
|
IWorkflowExecutionDataProcess,
|
||||||
|
@ -305,7 +306,8 @@ export class ExecuteBatch extends Command {
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
InternalHooksManager.init(instanceId);
|
const { cli } = await GenericHelpers.getVersions();
|
||||||
|
InternalHooksManager.init(instanceId, cli);
|
||||||
|
|
||||||
// Add the found types to an instance other parts of the application can use
|
// Add the found types to an instance other parts of the application can use
|
||||||
const nodeTypes = NodeTypes();
|
const nodeTypes = NodeTypes();
|
||||||
|
|
|
@ -153,17 +153,6 @@ export class Start extends Command {
|
||||||
LoggerProxy.init(logger);
|
LoggerProxy.init(logger);
|
||||||
logger.info('Initializing n8n process');
|
logger.info('Initializing n8n process');
|
||||||
|
|
||||||
logger.info(
|
|
||||||
'\n' +
|
|
||||||
'****************************************************\n' +
|
|
||||||
'* *\n' +
|
|
||||||
'* n8n now sends selected, anonymous telemetry. *\n' +
|
|
||||||
'* For more details (and how to opt out): *\n' +
|
|
||||||
'* https://docs.n8n.io/reference/telemetry.html *\n' +
|
|
||||||
'* *\n' +
|
|
||||||
'****************************************************\n',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Start directly with the init of the database to improve startup time
|
// Start directly with the init of the database to improve startup time
|
||||||
const startDbInitPromise = Db.init().catch((error: Error) => {
|
const startDbInitPromise = Db.init().catch((error: Error) => {
|
||||||
logger.error(`There was an error initializing DB: "${error.message}"`);
|
logger.error(`There was an error initializing DB: "${error.message}"`);
|
||||||
|
@ -313,7 +302,8 @@ export class Start extends Command {
|
||||||
}
|
}
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
InternalHooksManager.init(instanceId);
|
const { cli } = await GenericHelpers.getVersions();
|
||||||
|
InternalHooksManager.init(instanceId, cli);
|
||||||
|
|
||||||
await Server.start();
|
await Server.start();
|
||||||
|
|
||||||
|
|
|
@ -149,7 +149,8 @@ export class Webhook extends Command {
|
||||||
await startDbInitPromise;
|
await startDbInitPromise;
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
InternalHooksManager.init(instanceId);
|
const { cli } = await GenericHelpers.getVersions();
|
||||||
|
InternalHooksManager.init(instanceId, cli);
|
||||||
|
|
||||||
if (config.get('executions.mode') === 'queue') {
|
if (config.get('executions.mode') === 'queue') {
|
||||||
const redisHost = config.get('queue.bull.redis.host');
|
const redisHost = config.get('queue.bull.redis.host');
|
||||||
|
|
|
@ -271,10 +271,10 @@ export class Worker extends Command {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||||
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
Worker.jobQueue.process(flags.concurrency, async (job) => this.runJob(job, nodeTypes));
|
||||||
|
|
||||||
const instanceId = await UserSettings.getInstanceId();
|
|
||||||
InternalHooksManager.init(instanceId);
|
|
||||||
|
|
||||||
const versions = await GenericHelpers.getVersions();
|
const versions = await GenericHelpers.getVersions();
|
||||||
|
const instanceId = await UserSettings.getInstanceId();
|
||||||
|
|
||||||
|
InternalHooksManager.init(instanceId, versions.cli);
|
||||||
|
|
||||||
console.info('\nn8n worker is now ready');
|
console.info('\nn8n worker is now ready');
|
||||||
console.info(` * Version: ${versions.cli}`);
|
console.info(` * Version: ${versions.cli}`);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n",
|
"name": "n8n",
|
||||||
"version": "0.152.0",
|
"version": "0.153.0",
|
||||||
"description": "n8n Workflow Automation Tool",
|
"description": "n8n Workflow Automation Tool",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -112,8 +112,8 @@
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"mysql2": "~2.3.0",
|
"mysql2": "~2.3.0",
|
||||||
"n8n-core": "~0.96.0",
|
"n8n-core": "~0.96.0",
|
||||||
"n8n-editor-ui": "~0.119.0",
|
"n8n-editor-ui": "~0.120.0",
|
||||||
"n8n-nodes-base": "~0.149.0",
|
"n8n-nodes-base": "~0.150.0",
|
||||||
"n8n-workflow": "~0.79.0",
|
"n8n-workflow": "~0.79.0",
|
||||||
"oauth-1.0a": "^2.2.6",
|
"oauth-1.0a": "^2.2.6",
|
||||||
"open": "^7.0.0",
|
"open": "^7.0.0",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { ChildProcess } from 'child_process';
|
import { ChildProcess } from 'child_process';
|
||||||
|
import { stringify } from 'flatted';
|
||||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||||
import * as PCancelable from 'p-cancelable';
|
import * as PCancelable from 'p-cancelable';
|
||||||
// eslint-disable-next-line import/no-cycle
|
// eslint-disable-next-line import/no-cycle
|
||||||
|
@ -82,6 +83,7 @@ export class ActiveExecutions {
|
||||||
|
|
||||||
const execution = {
|
const execution = {
|
||||||
id: executionId,
|
id: executionId,
|
||||||
|
data: stringify(executionData.executionData!),
|
||||||
waitTill: null,
|
waitTill: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -314,7 +314,10 @@ export interface IDiagnosticInfo {
|
||||||
|
|
||||||
export interface IInternalHooksClass {
|
export interface IInternalHooksClass {
|
||||||
onN8nStop(): Promise<void>;
|
onN8nStop(): Promise<void>;
|
||||||
onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]>;
|
onServerStarted(
|
||||||
|
diagnosticInfo: IDiagnosticInfo,
|
||||||
|
firstWorkflowCreatedAt?: Date,
|
||||||
|
): Promise<unknown[]>;
|
||||||
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
|
onPersonalizationSurveySubmitted(answers: IPersonalizationSurveyAnswers): Promise<void>;
|
||||||
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
|
onWorkflowCreated(workflow: IWorkflowBase): Promise<void>;
|
||||||
onWorkflowDeleted(workflowId: string): Promise<void>;
|
onWorkflowDeleted(workflowId: string): Promise<void>;
|
||||||
|
@ -404,10 +407,12 @@ export interface IN8nUISettings {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPersonalizationSurveyAnswers {
|
export interface IPersonalizationSurveyAnswers {
|
||||||
companySize: string | null;
|
|
||||||
codingSkill: string | null;
|
codingSkill: string | null;
|
||||||
workArea: string | null;
|
companyIndustry: string[];
|
||||||
|
companySize: string | null;
|
||||||
|
otherCompanyIndustry: string | null;
|
||||||
otherWorkArea: string | null;
|
otherWorkArea: string | null;
|
||||||
|
workArea: string[] | string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPersonalizationSurvey {
|
export interface IPersonalizationSurvey {
|
||||||
|
|
|
@ -9,9 +9,16 @@ import {
|
||||||
import { Telemetry } from './telemetry';
|
import { Telemetry } from './telemetry';
|
||||||
|
|
||||||
export class InternalHooksClass implements IInternalHooksClass {
|
export class InternalHooksClass implements IInternalHooksClass {
|
||||||
constructor(private telemetry: Telemetry) {}
|
private versionCli: string;
|
||||||
|
|
||||||
async onServerStarted(diagnosticInfo: IDiagnosticInfo): Promise<unknown[]> {
|
constructor(private telemetry: Telemetry, versionCli: string) {
|
||||||
|
this.versionCli = versionCli;
|
||||||
|
}
|
||||||
|
|
||||||
|
async onServerStarted(
|
||||||
|
diagnosticInfo: IDiagnosticInfo,
|
||||||
|
earliestWorkflowCreatedAt?: Date,
|
||||||
|
): Promise<unknown[]> {
|
||||||
const info = {
|
const info = {
|
||||||
version_cli: diagnosticInfo.versionCli,
|
version_cli: diagnosticInfo.versionCli,
|
||||||
db_type: diagnosticInfo.databaseType,
|
db_type: diagnosticInfo.databaseType,
|
||||||
|
@ -25,7 +32,10 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
|
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
this.telemetry.identify(info),
|
this.telemetry.identify(info),
|
||||||
this.telemetry.track('Instance started', info),
|
this.telemetry.track('Instance started', {
|
||||||
|
...info,
|
||||||
|
earliest_workflow_created: earliestWorkflowCreatedAt,
|
||||||
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,13 +45,17 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
coding_skill: answers.codingSkill,
|
coding_skill: answers.codingSkill,
|
||||||
work_area: answers.workArea,
|
work_area: answers.workArea,
|
||||||
other_work_area: answers.otherWorkArea,
|
other_work_area: answers.otherWorkArea,
|
||||||
|
company_industry: answers.companyIndustry,
|
||||||
|
other_company_industry: answers.otherCompanyIndustry,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
|
async onWorkflowCreated(workflow: IWorkflowBase): Promise<void> {
|
||||||
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
|
||||||
return this.telemetry.track('User created workflow', {
|
return this.telemetry.track('User created workflow', {
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
|
node_graph: nodeGraph,
|
||||||
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,9 +66,13 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
|
async onWorkflowSaved(workflow: IWorkflowBase): Promise<void> {
|
||||||
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow);
|
||||||
|
|
||||||
return this.telemetry.track('User saved workflow', {
|
return this.telemetry.track('User saved workflow', {
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
node_graph: TelemetryHelpers.generateNodesGraph(workflow).nodeGraph,
|
node_graph: nodeGraph,
|
||||||
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
|
version_cli: this.versionCli,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,6 +80,7 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
const properties: IDataObject = {
|
const properties: IDataObject = {
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
is_manual: false,
|
is_manual: false,
|
||||||
|
version_cli: this.versionCli,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (runData !== undefined) {
|
if (runData !== undefined) {
|
||||||
|
@ -92,6 +111,8 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
if (properties.is_manual) {
|
if (properties.is_manual) {
|
||||||
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
|
const nodeGraphResult = TelemetryHelpers.generateNodesGraph(workflow);
|
||||||
properties.node_graph = nodeGraphResult.nodeGraph;
|
properties.node_graph = nodeGraphResult.nodeGraph;
|
||||||
|
properties.node_graph_string = JSON.stringify(nodeGraphResult.nodeGraph);
|
||||||
|
|
||||||
if (errorNodeName) {
|
if (errorNodeName) {
|
||||||
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
properties.error_node_id = nodeGraphResult.nameIndices[errorNodeName];
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,9 +13,12 @@ export class InternalHooksManager {
|
||||||
throw new Error('InternalHooks not initialized');
|
throw new Error('InternalHooks not initialized');
|
||||||
}
|
}
|
||||||
|
|
||||||
static init(instanceId: string): InternalHooksClass {
|
static init(instanceId: string, versionCli: string): InternalHooksClass {
|
||||||
if (!this.internalHooksInstance) {
|
if (!this.internalHooksInstance) {
|
||||||
this.internalHooksInstance = new InternalHooksClass(new Telemetry(instanceId));
|
this.internalHooksInstance = new InternalHooksClass(
|
||||||
|
new Telemetry(instanceId, versionCli),
|
||||||
|
versionCli,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.internalHooksInstance;
|
return this.internalHooksInstance;
|
||||||
|
|
|
@ -2952,7 +2952,23 @@ export async function start(): Promise<void> {
|
||||||
deploymentType: config.get('deployment.type'),
|
deploymentType: config.get('deployment.type'),
|
||||||
};
|
};
|
||||||
|
|
||||||
void InternalHooksManager.getInstance().onServerStarted(diagnosticInfo);
|
void Db.collections
|
||||||
|
.Workflow!.findOne({
|
||||||
|
select: ['createdAt'],
|
||||||
|
order: { createdAt: 'ASC' },
|
||||||
|
})
|
||||||
|
.then(async (workflow) =>
|
||||||
|
InternalHooksManager.getInstance().onServerStarted(diagnosticInfo, workflow?.createdAt),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
server.on('error', (error: Error & { code: string }) => {
|
||||||
|
if (error.code === 'EADDRINUSE') {
|
||||||
|
console.log(
|
||||||
|
`n8n's port ${PORT} is already in use. Do you have another instance of n8n running already?`,
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -509,7 +509,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
this.workflowData,
|
this.workflowData,
|
||||||
fullRunData,
|
fullRunData,
|
||||||
this.mode,
|
this.mode,
|
||||||
undefined,
|
this.executionId,
|
||||||
this.retryOf,
|
this.retryOf,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -585,7 +585,7 @@ function hookFunctionsSave(parentProcessMode?: string): IWorkflowExecuteHooks {
|
||||||
this.workflowData,
|
this.workflowData,
|
||||||
fullRunData,
|
fullRunData,
|
||||||
this.mode,
|
this.mode,
|
||||||
undefined,
|
this.executionId,
|
||||||
this.retryOf,
|
this.retryOf,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -635,7 +635,7 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
this.workflowData,
|
this.workflowData,
|
||||||
fullRunData,
|
fullRunData,
|
||||||
this.mode,
|
this.mode,
|
||||||
undefined,
|
this.executionId,
|
||||||
this.retryOf,
|
this.retryOf,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -676,7 +676,13 @@ function hookFunctionsSaveWorker(): IWorkflowExecuteHooks {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
executeErrorWorkflow(this.workflowData, fullRunData, this.mode, undefined, this.retryOf);
|
executeErrorWorkflow(
|
||||||
|
this.workflowData,
|
||||||
|
fullRunData,
|
||||||
|
this.mode,
|
||||||
|
this.executionId,
|
||||||
|
this.retryOf,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -31,6 +31,7 @@ import {
|
||||||
CredentialTypes,
|
CredentialTypes,
|
||||||
Db,
|
Db,
|
||||||
ExternalHooks,
|
ExternalHooks,
|
||||||
|
GenericHelpers,
|
||||||
IWorkflowExecuteProcess,
|
IWorkflowExecuteProcess,
|
||||||
IWorkflowExecutionDataProcessWithExecution,
|
IWorkflowExecutionDataProcessWithExecution,
|
||||||
NodeTypes,
|
NodeTypes,
|
||||||
|
@ -137,7 +138,8 @@ export class WorkflowRunnerProcess {
|
||||||
await externalHooks.init();
|
await externalHooks.init();
|
||||||
|
|
||||||
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
const instanceId = (await UserSettings.prepareUserSettings()).instanceId ?? '';
|
||||||
InternalHooksManager.init(instanceId);
|
const { cli } = await GenericHelpers.getVersions();
|
||||||
|
InternalHooksManager.init(instanceId, cli);
|
||||||
|
|
||||||
// Credentials should now be loaded from database.
|
// Credentials should now be loaded from database.
|
||||||
// We check if any node uses credentials. If it does, then
|
// We check if any node uses credentials. If it does, then
|
||||||
|
|
|
@ -5,28 +5,57 @@ import { IDataObject, LoggerProxy } from 'n8n-workflow';
|
||||||
import config = require('../../config');
|
import config = require('../../config');
|
||||||
import { getLogger } from '../Logger';
|
import { getLogger } from '../Logger';
|
||||||
|
|
||||||
interface IExecutionCountsBufferItem {
|
type CountBufferItemKey =
|
||||||
manual_success_count: number;
|
| 'manual_success_count'
|
||||||
manual_error_count: number;
|
| 'manual_error_count'
|
||||||
prod_success_count: number;
|
| 'prod_success_count'
|
||||||
prod_error_count: number;
|
| 'prod_error_count';
|
||||||
}
|
|
||||||
|
type FirstExecutionItemKey =
|
||||||
|
| 'first_manual_success'
|
||||||
|
| 'first_manual_error'
|
||||||
|
| 'first_prod_success'
|
||||||
|
| 'first_prod_error';
|
||||||
|
|
||||||
|
type IExecutionCountsBufferItem = {
|
||||||
|
[key in CountBufferItemKey]: number;
|
||||||
|
};
|
||||||
|
|
||||||
interface IExecutionCountsBuffer {
|
interface IExecutionCountsBuffer {
|
||||||
[workflowId: string]: IExecutionCountsBufferItem;
|
[workflowId: string]: IExecutionCountsBufferItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type IFirstExecutions = {
|
||||||
|
[key in FirstExecutionItemKey]: Date | undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IExecutionsBuffer {
|
||||||
|
counts: IExecutionCountsBuffer;
|
||||||
|
firstExecutions: IFirstExecutions;
|
||||||
|
}
|
||||||
|
|
||||||
export class Telemetry {
|
export class Telemetry {
|
||||||
private client?: TelemetryClient;
|
private client?: TelemetryClient;
|
||||||
|
|
||||||
private instanceId: string;
|
private instanceId: string;
|
||||||
|
|
||||||
|
private versionCli: string;
|
||||||
|
|
||||||
private pulseIntervalReference: NodeJS.Timeout;
|
private pulseIntervalReference: NodeJS.Timeout;
|
||||||
|
|
||||||
private executionCountsBuffer: IExecutionCountsBuffer = {};
|
private executionCountsBuffer: IExecutionsBuffer = {
|
||||||
|
counts: {},
|
||||||
|
firstExecutions: {
|
||||||
|
first_manual_error: undefined,
|
||||||
|
first_manual_success: undefined,
|
||||||
|
first_prod_error: undefined,
|
||||||
|
first_prod_success: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
constructor(instanceId: string) {
|
constructor(instanceId: string, versionCli: string) {
|
||||||
this.instanceId = instanceId;
|
this.instanceId = instanceId;
|
||||||
|
this.versionCli = versionCli;
|
||||||
|
|
||||||
const enabled = config.get('diagnostics.enabled') as boolean;
|
const enabled = config.get('diagnostics.enabled') as boolean;
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
@ -53,33 +82,41 @@ export class Telemetry {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
const allPromises = Object.keys(this.executionCountsBuffer).map(async (workflowId) => {
|
const allPromises = Object.keys(this.executionCountsBuffer.counts).map(async (workflowId) => {
|
||||||
const promise = this.track('Workflow execution count', {
|
const promise = this.track('Workflow execution count', {
|
||||||
|
version_cli: this.versionCli,
|
||||||
workflow_id: workflowId,
|
workflow_id: workflowId,
|
||||||
...this.executionCountsBuffer[workflowId],
|
...this.executionCountsBuffer.counts[workflowId],
|
||||||
|
...this.executionCountsBuffer.firstExecutions,
|
||||||
});
|
});
|
||||||
this.executionCountsBuffer[workflowId].manual_error_count = 0;
|
|
||||||
this.executionCountsBuffer[workflowId].manual_success_count = 0;
|
this.executionCountsBuffer.counts[workflowId].manual_error_count = 0;
|
||||||
this.executionCountsBuffer[workflowId].prod_error_count = 0;
|
this.executionCountsBuffer.counts[workflowId].manual_success_count = 0;
|
||||||
this.executionCountsBuffer[workflowId].prod_success_count = 0;
|
this.executionCountsBuffer.counts[workflowId].prod_error_count = 0;
|
||||||
|
this.executionCountsBuffer.counts[workflowId].prod_success_count = 0;
|
||||||
|
|
||||||
return promise;
|
return promise;
|
||||||
});
|
});
|
||||||
|
|
||||||
allPromises.push(this.track('pulse'));
|
allPromises.push(this.track('pulse', { version_cli: this.versionCli }));
|
||||||
return Promise.all(allPromises);
|
return Promise.all(allPromises);
|
||||||
}
|
}
|
||||||
|
|
||||||
async trackWorkflowExecution(properties: IDataObject): Promise<void> {
|
async trackWorkflowExecution(properties: IDataObject): Promise<void> {
|
||||||
if (this.client) {
|
if (this.client) {
|
||||||
const workflowId = properties.workflow_id as string;
|
const workflowId = properties.workflow_id as string;
|
||||||
this.executionCountsBuffer[workflowId] = this.executionCountsBuffer[workflowId] ?? {
|
this.executionCountsBuffer.counts[workflowId] = this.executionCountsBuffer.counts[
|
||||||
|
workflowId
|
||||||
|
] ?? {
|
||||||
manual_error_count: 0,
|
manual_error_count: 0,
|
||||||
manual_success_count: 0,
|
manual_success_count: 0,
|
||||||
prod_error_count: 0,
|
prod_error_count: 0,
|
||||||
prod_success_count: 0,
|
prod_success_count: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let countKey: CountBufferItemKey;
|
||||||
|
let firstExecKey: FirstExecutionItemKey;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
properties.success === false &&
|
properties.success === false &&
|
||||||
properties.error_node_type &&
|
properties.error_node_type &&
|
||||||
|
@ -89,15 +126,28 @@ export class Telemetry {
|
||||||
void this.track('Workflow execution errored', properties);
|
void this.track('Workflow execution errored', properties);
|
||||||
|
|
||||||
if (properties.is_manual) {
|
if (properties.is_manual) {
|
||||||
this.executionCountsBuffer[workflowId].manual_error_count++;
|
firstExecKey = 'first_manual_error';
|
||||||
|
countKey = 'manual_error_count';
|
||||||
} else {
|
} else {
|
||||||
this.executionCountsBuffer[workflowId].prod_error_count++;
|
firstExecKey = 'first_prod_error';
|
||||||
|
countKey = 'prod_error_count';
|
||||||
}
|
}
|
||||||
} else if (properties.is_manual) {
|
} else if (properties.is_manual) {
|
||||||
this.executionCountsBuffer[workflowId].manual_success_count++;
|
countKey = 'manual_success_count';
|
||||||
|
firstExecKey = 'first_manual_success';
|
||||||
} else {
|
} else {
|
||||||
this.executionCountsBuffer[workflowId].prod_success_count++;
|
countKey = 'prod_success_count';
|
||||||
|
firstExecKey = 'first_prod_success';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.executionCountsBuffer.firstExecutions[firstExecKey] &&
|
||||||
|
this.executionCountsBuffer.counts[workflowId][countKey] === 0
|
||||||
|
) {
|
||||||
|
this.executionCountsBuffer.firstExecutions[firstExecKey] = new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.executionCountsBuffer.counts[workflowId][countKey]++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-design-system",
|
"name": "n8n-design-system",
|
||||||
"version": "0.8.0",
|
"version": "0.9.0",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
"author": {
|
"author": {
|
||||||
|
|
|
@ -13,7 +13,7 @@ export default {
|
||||||
color: {
|
color: {
|
||||||
control: {
|
control: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: ['primary', 'text-dark', 'text-base', 'text-light'],
|
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default {
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light'].includes(value),
|
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'].includes(value),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import N8nSquareButton from './SquareButton.vue';
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/SquareButton',
|
||||||
|
component: N8nSquareButton,
|
||||||
|
argTypes: {
|
||||||
|
label: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
onClick: action('click'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nSquareButton,
|
||||||
|
},
|
||||||
|
template: '<n8n-square-button v-bind="$props" @click="onClick"></n8n-square-button>',
|
||||||
|
methods,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const SquareButton = Template.bind({});
|
|
@ -0,0 +1,43 @@
|
||||||
|
<template functional>
|
||||||
|
<button :class="$style.button" @click="(e) => listeners.click && listeners.click(e)">
|
||||||
|
<span :class="$style.text" v-text="props.label" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
export default Vue.extend({
|
||||||
|
name: 'n8n-square-button',
|
||||||
|
props: {
|
||||||
|
label: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.button {
|
||||||
|
width: 28px;
|
||||||
|
height: 29px;
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
border: var(--color-background-xlight);
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.text {
|
||||||
|
color: var(--color-primary) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
color: var(--color-background-dark);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,3 @@
|
||||||
|
import N8nSquareButton from './SquareButton.vue';
|
||||||
|
|
||||||
|
export default N8nSquareButton;
|
|
@ -13,7 +13,7 @@ export default {
|
||||||
color: {
|
color: {
|
||||||
control: {
|
control: {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
options: ['primary', 'text-dark', 'text-base', 'text-light'],
|
options: ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default Vue.extend({
|
||||||
},
|
},
|
||||||
color: {
|
color: {
|
||||||
type: String,
|
type: String,
|
||||||
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light'].includes(value),
|
validator: (value: string): boolean => ['primary', 'text-dark', 'text-base', 'text-light', 'text-xlight'].includes(value),
|
||||||
},
|
},
|
||||||
align: {
|
align: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|
|
@ -10,6 +10,7 @@ import N8nMenu from './N8nMenu';
|
||||||
import N8nMenuItem from './N8nMenuItem';
|
import N8nMenuItem from './N8nMenuItem';
|
||||||
import N8nSelect from './N8nSelect';
|
import N8nSelect from './N8nSelect';
|
||||||
import N8nSpinner from './N8nSpinner';
|
import N8nSpinner from './N8nSpinner';
|
||||||
|
import N8nSquareButton from './N8nSquareButton';
|
||||||
import N8nText from './N8nText';
|
import N8nText from './N8nText';
|
||||||
import N8nTooltip from './N8nTooltip';
|
import N8nTooltip from './N8nTooltip';
|
||||||
import N8nOption from './N8nOption';
|
import N8nOption from './N8nOption';
|
||||||
|
@ -27,6 +28,7 @@ export {
|
||||||
N8nMenuItem,
|
N8nMenuItem,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nSquareButton,
|
||||||
N8nText,
|
N8nText,
|
||||||
N8nTooltip,
|
N8nTooltip,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-editor-ui",
|
"name": "n8n-editor-ui",
|
||||||
"version": "0.119.0",
|
"version": "0.120.0",
|
||||||
"description": "Workflow Editor UI for n8n",
|
"description": "Workflow Editor UI for n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -26,7 +26,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^4.5.0",
|
"@fontsource/open-sans": "^4.5.0",
|
||||||
"n8n-design-system": "~0.8.0",
|
"n8n-design-system": "~0.9.0",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-fragment": "^1.5.2",
|
"vue-fragment": "^1.5.2",
|
||||||
|
|
|
@ -14,14 +14,20 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
import Telemetry from './components/Telemetry.vue';
|
import Telemetry from './components/Telemetry.vue';
|
||||||
|
|
||||||
export default {
|
export default Vue.extend({
|
||||||
name: 'App',
|
name: 'App',
|
||||||
components: {
|
components: {
|
||||||
Telemetry,
|
Telemetry,
|
||||||
},
|
},
|
||||||
};
|
watch: {
|
||||||
|
'$route'(route) {
|
||||||
|
this.$telemetry.page('Editor', route.name);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -496,6 +496,21 @@ export interface IPersonalizationSurvey {
|
||||||
shouldShow: boolean;
|
shouldShow: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IN8nPrompts {
|
||||||
|
message: string;
|
||||||
|
title: string;
|
||||||
|
showContactPrompt: boolean;
|
||||||
|
showValueSurvey: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IN8nValueSurveyData {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IN8nPromptResponse {
|
||||||
|
updated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IN8nUISettings {
|
export interface IN8nUISettings {
|
||||||
endpointWebhook: string;
|
endpointWebhook: string;
|
||||||
endpointWebhookTest: string;
|
endpointWebhookTest: string;
|
||||||
|
@ -703,6 +718,7 @@ export interface IUiState {
|
||||||
|
|
||||||
export interface ISettingsState {
|
export interface ISettingsState {
|
||||||
settings: IN8nUISettings;
|
settings: IN8nUISettings;
|
||||||
|
promptsData: IN8nPrompts;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IVersionsState {
|
export interface IVersionsState {
|
||||||
|
|
|
@ -93,3 +93,7 @@ export async function makeRestApiRequest(context: IRestApiContext, method: Metho
|
||||||
export async function get(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) {
|
export async function get(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) {
|
||||||
return await request({method: 'GET', baseURL, endpoint, headers, data: params});
|
return await request({method: 'GET', baseURL, endpoint, headers, data: params});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function post(baseURL: string, endpoint: string, params?: IDataObject, headers?: IDataObject) {
|
||||||
|
return await request({method: 'POST', baseURL, endpoint, headers, data: params});
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { IDataObject } from 'n8n-workflow';
|
import { IDataObject } from 'n8n-workflow';
|
||||||
import { IRestApiContext, IN8nUISettings, IPersonalizationSurveyAnswers } from '../Interface';
|
import { IRestApiContext, IN8nPrompts, IN8nValueSurveyData, IN8nUISettings, IPersonalizationSurveyAnswers } from '../Interface';
|
||||||
import { makeRestApiRequest } from './helpers';
|
import { makeRestApiRequest, get, post } from './helpers';
|
||||||
|
import { TEMPLATES_BASE_URL } from '@/constants';
|
||||||
|
|
||||||
export async function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
|
export async function getSettings(context: IRestApiContext): Promise<IN8nUISettings> {
|
||||||
return await makeRestApiRequest(context, 'GET', '/settings');
|
return await makeRestApiRequest(context, 'GET', '/settings');
|
||||||
|
@ -10,3 +11,15 @@ export async function submitPersonalizationSurvey(context: IRestApiContext, para
|
||||||
await makeRestApiRequest(context, 'POST', '/user-survey', params as unknown as IDataObject);
|
await makeRestApiRequest(context, 'POST', '/user-survey', params as unknown as IDataObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getPromptsData(instanceId: string): Promise<IN8nPrompts> {
|
||||||
|
return await get(TEMPLATES_BASE_URL, '/prompts', {}, {'n8n-instance-id': instanceId});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitContactInfo(instanceId: string, email: string): Promise<void> {
|
||||||
|
return await post(TEMPLATES_BASE_URL, '/prompt', { email }, {'n8n-instance-id': instanceId});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitValueSurvey(instanceId: string, params: IN8nValueSurveyData): Promise<IN8nPrompts> {
|
||||||
|
return await post(TEMPLATES_BASE_URL, '/value-survey', params, {'n8n-instance-id': instanceId});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
128
packages/editor-ui/src/components/ContactPromptModal.vue
Normal file
128
packages/editor-ui/src/components/ContactPromptModal.vue
Normal file
|
@ -0,0 +1,128 @@
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
:name="modalName"
|
||||||
|
:eventBus="modalBus"
|
||||||
|
:center="true"
|
||||||
|
:closeOnPressEscape="false"
|
||||||
|
:beforeClose="closeDialog"
|
||||||
|
customClass="contact-prompt-modal"
|
||||||
|
width="460px"
|
||||||
|
>
|
||||||
|
<template slot="header">
|
||||||
|
<n8n-heading tag="h2" size="xlarge" color="text-dark">{{ title }}</n8n-heading>
|
||||||
|
</template>
|
||||||
|
<template v-slot:content>
|
||||||
|
<div :class="$style.description">
|
||||||
|
<n8n-text size="medium" color="text-base">{{ description }}</n8n-text>
|
||||||
|
</div>
|
||||||
|
<div @keyup.enter="send">
|
||||||
|
<n8n-input v-model="email" placeholder="Your email address" />
|
||||||
|
</div>
|
||||||
|
<div :class="$style.disclaimer">
|
||||||
|
<n8n-text size="small" color="text-base"
|
||||||
|
>David from our product team will get in touch personally</n8n-text
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<div :class="$style.footer">
|
||||||
|
<n8n-button label="Send" float="right" @click="send" :disabled="!isEmailValid" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
import { IN8nPromptResponse } from '@/Interface';
|
||||||
|
import { VALID_EMAIL_REGEX } from '@/constants';
|
||||||
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
import Modal from './Modal.vue';
|
||||||
|
|
||||||
|
export default mixins(workflowHelpers).extend({
|
||||||
|
components: { Modal },
|
||||||
|
name: 'ContactPromptModal',
|
||||||
|
props: ['modalName'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
email: '',
|
||||||
|
modalBus: new Vue(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
promptsData: 'settings/getPromptsData',
|
||||||
|
}),
|
||||||
|
title(): string {
|
||||||
|
if (this.promptsData && this.promptsData.title) {
|
||||||
|
return this.promptsData.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'You’re a power user 💪';
|
||||||
|
},
|
||||||
|
description(): string {
|
||||||
|
if (this.promptsData && this.promptsData.message) {
|
||||||
|
return this.promptsData.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Your experience with n8n can help us improve — for you and our entire community.';
|
||||||
|
},
|
||||||
|
isEmailValid(): boolean {
|
||||||
|
return VALID_EMAIL_REGEX.test(String(this.email).toLowerCase());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDialog(): void {
|
||||||
|
this.$telemetry.track('User closed email modal', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
email: null,
|
||||||
|
});
|
||||||
|
this.$store.commit('ui/closeTopModal');
|
||||||
|
},
|
||||||
|
async send() {
|
||||||
|
if (this.isEmailValid) {
|
||||||
|
const response: IN8nPromptResponse = await this.$store.dispatch(
|
||||||
|
'settings/submitContactInfo',
|
||||||
|
this.email,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.updated) {
|
||||||
|
this.$telemetry.track('User closed email modal', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
email: this.email,
|
||||||
|
});
|
||||||
|
this.$showMessage({
|
||||||
|
title: 'Thanks!',
|
||||||
|
message: "It's people like you that help make n8n better",
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.$store.commit('ui/closeTopModal');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.description {
|
||||||
|
margin-bottom: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer {
|
||||||
|
margin-top: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.dialog-wrapper {
|
||||||
|
.contact-prompt-modal {
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 16px 24px 24px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -140,8 +140,9 @@ export default mixins(workflowHelpers).extend({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
onSaveButtonClick () {
|
async onSaveButtonClick () {
|
||||||
this.saveCurrentWorkflow(undefined);
|
const saved = await this.saveCurrentWorkflow();
|
||||||
|
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||||
},
|
},
|
||||||
onTagsEditEnable() {
|
onTagsEditEnable() {
|
||||||
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
this.$data.appliedTagIds = this.currentWorkflowTagIds;
|
||||||
|
|
|
@ -452,7 +452,8 @@ export default mixins(
|
||||||
|
|
||||||
saveAs(blob, workflowName + '.json');
|
saveAs(blob, workflowName + '.json');
|
||||||
} else if (key === 'workflow-save') {
|
} else if (key === 'workflow-save') {
|
||||||
this.saveCurrentWorkflow(undefined);
|
const saved = await this.saveCurrentWorkflow();
|
||||||
|
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||||
} else if (key === 'workflow-duplicate') {
|
} else if (key === 'workflow-duplicate') {
|
||||||
this.$store.dispatch('ui/openModal', DUPLICATE_MODAL_KEY);
|
this.$store.dispatch('ui/openModal', DUPLICATE_MODAL_KEY);
|
||||||
} else if (key === 'help-about') {
|
} else if (key === 'help-about') {
|
||||||
|
|
|
@ -3,7 +3,9 @@
|
||||||
:direction="direction"
|
:direction="direction"
|
||||||
:visible="visible"
|
:visible="visible"
|
||||||
:size="width"
|
:size="width"
|
||||||
:before-close="close"
|
:before-close="beforeClose"
|
||||||
|
:modal="modal"
|
||||||
|
:wrapperClosable="wrapperClosable"
|
||||||
>
|
>
|
||||||
<template v-slot:title>
|
<template v-slot:title>
|
||||||
<slot name="header" />
|
<slot name="header" />
|
||||||
|
@ -23,15 +25,26 @@ export default Vue.extend({
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
beforeClose: {
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
eventBus: {
|
eventBus: {
|
||||||
type: Vue,
|
type: Vue,
|
||||||
},
|
},
|
||||||
direction: {
|
direction: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
modal: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
width: {
|
width: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
wrapperClosable: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('keydown', this.onWindowKeydown);
|
window.addEventListener('keydown', this.onWindowKeydown);
|
||||||
|
|
|
@ -1,5 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<ModalRoot :name="CONTACT_PROMPT_MODAL_KEY">
|
||||||
|
<template v-slot:default="{ modalName }">
|
||||||
|
<ContactPromptModal
|
||||||
|
:modalName="modalName"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
<ModalRoot :name="CREDENTIAL_EDIT_MODAL_KEY">
|
<ModalRoot :name="CREDENTIAL_EDIT_MODAL_KEY">
|
||||||
<template v-slot="{ modalName, activeId, mode }">
|
<template v-slot="{ modalName, activeId, mode }">
|
||||||
<CredentialEdit
|
<CredentialEdit
|
||||||
|
@ -39,6 +47,12 @@
|
||||||
<UpdatesPanel />
|
<UpdatesPanel />
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
|
||||||
|
<ModalRoot :name="VALUE_SURVEY_MODAL_KEY" :keepAlive="true">
|
||||||
|
<template v-slot:default="{ active }">
|
||||||
|
<ValueSurvey :isActive="active"/>
|
||||||
|
</template>
|
||||||
|
</ModalRoot>
|
||||||
|
|
||||||
<ModalRoot :name="WORKFLOW_OPEN_MODAL_KEY">
|
<ModalRoot :name="WORKFLOW_OPEN_MODAL_KEY">
|
||||||
<WorkflowOpen />
|
<WorkflowOpen />
|
||||||
</ModalRoot>
|
</ModalRoot>
|
||||||
|
@ -51,8 +65,9 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Vue from "vue";
|
import Vue from "vue";
|
||||||
import { CREDENTIAL_LIST_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, PERSONALIZATION_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY } from '@/constants';
|
import { CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, PERSONALIZATION_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, VERSIONS_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
|
import ContactPromptModal from './ContactPromptModal.vue';
|
||||||
import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
import CredentialEdit from "./CredentialEdit/CredentialEdit.vue";
|
||||||
import CredentialsList from "./CredentialsList.vue";
|
import CredentialsList from "./CredentialsList.vue";
|
||||||
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
import CredentialsSelectModal from "./CredentialsSelectModal.vue";
|
||||||
|
@ -61,12 +76,14 @@ import ModalRoot from "./ModalRoot.vue";
|
||||||
import PersonalizationModal from "./PersonalizationModal.vue";
|
import PersonalizationModal from "./PersonalizationModal.vue";
|
||||||
import TagsManager from "./TagsManager/TagsManager.vue";
|
import TagsManager from "./TagsManager/TagsManager.vue";
|
||||||
import UpdatesPanel from "./UpdatesPanel.vue";
|
import UpdatesPanel from "./UpdatesPanel.vue";
|
||||||
|
import ValueSurvey from "./ValueSurvey.vue";
|
||||||
import WorkflowSettings from "./WorkflowSettings.vue";
|
import WorkflowSettings from "./WorkflowSettings.vue";
|
||||||
import WorkflowOpen from "./WorkflowOpen.vue";
|
import WorkflowOpen from "./WorkflowOpen.vue";
|
||||||
|
|
||||||
export default Vue.extend({
|
export default Vue.extend({
|
||||||
name: "Modals",
|
name: "Modals",
|
||||||
components: {
|
components: {
|
||||||
|
ContactPromptModal,
|
||||||
CredentialEdit,
|
CredentialEdit,
|
||||||
CredentialsList,
|
CredentialsList,
|
||||||
CredentialsSelectModal,
|
CredentialsSelectModal,
|
||||||
|
@ -75,10 +92,12 @@ export default Vue.extend({
|
||||||
PersonalizationModal,
|
PersonalizationModal,
|
||||||
TagsManager,
|
TagsManager,
|
||||||
UpdatesPanel,
|
UpdatesPanel,
|
||||||
|
ValueSurvey,
|
||||||
WorkflowSettings,
|
WorkflowSettings,
|
||||||
WorkflowOpen,
|
WorkflowOpen,
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
|
CONTACT_PROMPT_MODAL_KEY,
|
||||||
CREDENTIAL_EDIT_MODAL_KEY,
|
CREDENTIAL_EDIT_MODAL_KEY,
|
||||||
CREDENTIAL_LIST_MODAL_KEY,
|
CREDENTIAL_LIST_MODAL_KEY,
|
||||||
CREDENTIAL_SELECT_MODAL_KEY,
|
CREDENTIAL_SELECT_MODAL_KEY,
|
||||||
|
@ -88,6 +107,7 @@ export default Vue.extend({
|
||||||
VERSIONS_MODAL_KEY,
|
VERSIONS_MODAL_KEY,
|
||||||
WORKFLOW_OPEN_MODAL_KEY,
|
WORKFLOW_OPEN_MODAL_KEY,
|
||||||
WORKFLOW_SETTINGS_MODAL_KEY,
|
WORKFLOW_SETTINGS_MODAL_KEY,
|
||||||
|
VALUE_SURVEY_MODAL_KEY,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -128,6 +128,9 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
return `Waiting for you to create an event in ${this.nodeType && this.nodeType.displayName.replace(/Trigger/, "")}`;
|
return `Waiting for you to create an event in ${this.nodeType && this.nodeType.displayName.replace(/Trigger/, "")}`;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
isPollingTypeNode (): boolean {
|
||||||
|
return !!(this.nodeType && this.nodeType.polling);
|
||||||
|
},
|
||||||
isExecuting (): boolean {
|
isExecuting (): boolean {
|
||||||
return this.$store.getters.executingNode === this.data.name;
|
return this.$store.getters.executingNode === this.data.name;
|
||||||
},
|
},
|
||||||
|
@ -266,7 +269,16 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
return !!(this.nodeType && this.nodeType.outputs.length > 2);
|
return !!(this.nodeType && this.nodeType.outputs.length > 2);
|
||||||
},
|
},
|
||||||
shouldShowTriggerTooltip () : boolean {
|
shouldShowTriggerTooltip () : boolean {
|
||||||
return !!this.node && this.workflowRunning && this.workflowDataItems === 0 && this.isTriggerNode && this.isSingleActiveTriggerNode && !this.isTriggerNodeTooltipEmpty && !this.isNodeDisabled && !this.hasIssues && !this.dragging;
|
return !!this.node &&
|
||||||
|
this.isTriggerNode &&
|
||||||
|
!this.isPollingTypeNode &&
|
||||||
|
!this.isNodeDisabled &&
|
||||||
|
this.workflowRunning &&
|
||||||
|
this.workflowDataItems === 0 &&
|
||||||
|
this.isSingleActiveTriggerNode &&
|
||||||
|
!this.isTriggerNodeTooltipEmpty &&
|
||||||
|
!this.hasIssues &&
|
||||||
|
!this.dragging;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
@ -478,7 +490,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||||
top: -25px;
|
top: -25px;
|
||||||
left: -10px;
|
left: -10px;
|
||||||
width: 120px;
|
width: 120px;
|
||||||
height: 24px;
|
height: 26px;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
|
|
283
packages/editor-ui/src/components/ValueSurvey.vue
Normal file
283
packages/editor-ui/src/components/ValueSurvey.vue
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
<template>
|
||||||
|
<ModalDrawer
|
||||||
|
:name="VALUE_SURVEY_MODAL_KEY"
|
||||||
|
:beforeClose="closeDialog"
|
||||||
|
:modal="false"
|
||||||
|
:wrapperClosable="false"
|
||||||
|
direction="btt"
|
||||||
|
width="120px"
|
||||||
|
class="value-survey"
|
||||||
|
>
|
||||||
|
<template slot="header">
|
||||||
|
<div :class="$style.title">
|
||||||
|
<n8n-heading tag="h2" size="medium" color="text-xlight">{{ getTitle }}</n8n-heading>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template slot="content">
|
||||||
|
<section :class="$style.content">
|
||||||
|
<div v-if="showButtons" :class="$style.wrapper">
|
||||||
|
<div :class="$style.buttons">
|
||||||
|
<div v-for="value in 11" :key="value - 1" :class="$style.container">
|
||||||
|
<n8n-square-button
|
||||||
|
:label="(value - 1).toString()"
|
||||||
|
@click="selectSurveyValue((value - 1).toString())"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.text">
|
||||||
|
<n8n-text size="small" color="text-xlight">Not likely</n8n-text>
|
||||||
|
<n8n-text size="small" color="text-xlight">Very likely</n8n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else :class="$style.email">
|
||||||
|
<div :class="$style.input" @keyup.enter="send">
|
||||||
|
<n8n-input
|
||||||
|
v-model="form.email"
|
||||||
|
placeholder="Your email address"
|
||||||
|
size="medium"
|
||||||
|
@input="onInputChange"
|
||||||
|
/>
|
||||||
|
<div :class="$style.button">
|
||||||
|
<n8n-button label="Send" float="right" @click="send" :disabled="!isEmailValid" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.disclaimer">
|
||||||
|
<n8n-text size="small" color="text-xlight">
|
||||||
|
David from our product team will get in touch personally
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
</ModalDrawer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { VALID_EMAIL_REGEX, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||||
|
import { IN8nPromptResponse } from '@/Interface';
|
||||||
|
|
||||||
|
import ModalDrawer from './ModalDrawer.vue';
|
||||||
|
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
|
||||||
|
const DEFAULT_TITLE = `How likely are you to recommend n8n to a friend or colleague?`;
|
||||||
|
const GREAT_FEEDBACK_TITLE = `Great to hear! Can we reach out to see how we can make n8n even better for you?`;
|
||||||
|
const DEFAULT_FEEDBACK_TITLE = `Thanks for your feedback! We'd love to understand how we can improve. Can we reach out?`;
|
||||||
|
|
||||||
|
export default mixins(workflowHelpers).extend({
|
||||||
|
name: 'ValueSurvey',
|
||||||
|
props: ['isActive'],
|
||||||
|
components: {
|
||||||
|
ModalDrawer,
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
isActive(isActive) {
|
||||||
|
if (isActive) {
|
||||||
|
this.$telemetry.track('User shown value survey', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getTitle(): string {
|
||||||
|
if (this.form.value !== '') {
|
||||||
|
if (Number(this.form.value) > 7) {
|
||||||
|
return GREAT_FEEDBACK_TITLE;
|
||||||
|
} else {
|
||||||
|
return DEFAULT_FEEDBACK_TITLE;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return DEFAULT_TITLE;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isEmailValid(): boolean {
|
||||||
|
return VALID_EMAIL_REGEX.test(String(this.form.email).toLowerCase());
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
form: {
|
||||||
|
email: '',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
showButtons: true,
|
||||||
|
VALUE_SURVEY_MODAL_KEY,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDialog(): void {
|
||||||
|
if (this.form.value === '') {
|
||||||
|
this.$telemetry.track('User responded value survey score', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
nps: '',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.$telemetry.track('User responded value survey email', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
email: '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.commit('ui/closeTopModal');
|
||||||
|
},
|
||||||
|
onInputChange(value: string) {
|
||||||
|
this.form.email = value;
|
||||||
|
},
|
||||||
|
async selectSurveyValue(value: string) {
|
||||||
|
this.form.value = value;
|
||||||
|
this.showButtons = false;
|
||||||
|
|
||||||
|
const response: IN8nPromptResponse = await this.$store.dispatch(
|
||||||
|
'settings/submitValueSurvey',
|
||||||
|
{ value: this.form.value },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.updated) {
|
||||||
|
this.$telemetry.track('User responded value survey score', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
nps: this.form.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async send() {
|
||||||
|
if (this.isEmailValid) {
|
||||||
|
const response: IN8nPromptResponse = await this.$store.dispatch(
|
||||||
|
'settings/submitValueSurvey',
|
||||||
|
{
|
||||||
|
email: this.form.email,
|
||||||
|
value: this.form.value,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.updated) {
|
||||||
|
this.$telemetry.track('User responded value survey email', {
|
||||||
|
instance_id: this.$store.getters.instanceId,
|
||||||
|
email: this.form.email,
|
||||||
|
});
|
||||||
|
this.$showMessage({
|
||||||
|
title: 'Thanks for your feedback',
|
||||||
|
message: `If you’d like to help even more, answer this <a target="_blank" href="https://n8n-community.typeform.com/quicksurvey#nps=${this.form.value}&instance_id=${this.$store.getters.instanceId}">quick survey.</a>`,
|
||||||
|
type: 'success',
|
||||||
|
duration: 15000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.form.value = '';
|
||||||
|
this.form.email = '';
|
||||||
|
this.showButtons = true;
|
||||||
|
}, 1000);
|
||||||
|
this.$store.commit('ui/closeTopModal');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style module lang="scss">
|
||||||
|
.title {
|
||||||
|
height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (max-width: $--breakpoint-xs) {
|
||||||
|
margin-top: 10px;
|
||||||
|
padding: 0 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
@media (max-width: $--breakpoint-xs) {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
margin: 0 8px;
|
||||||
|
|
||||||
|
@media (max-width: $--breakpoint-xs) {
|
||||||
|
margin: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
margin-top: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disclaimer {
|
||||||
|
margin-top: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.value-survey {
|
||||||
|
height: 120px;
|
||||||
|
top: auto;
|
||||||
|
|
||||||
|
@media (max-width: $--breakpoint-xs) {
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-drawer {
|
||||||
|
background: var(--color-background-dark);
|
||||||
|
|
||||||
|
@media (max-width: $--breakpoint-xs) {
|
||||||
|
height: 140px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
height: 50px;
|
||||||
|
margin: 0;
|
||||||
|
padding: 18px 0 16px;
|
||||||
|
|
||||||
|
.el-drawer__close-btn {
|
||||||
|
top: 12px;
|
||||||
|
right: 16px;
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
@media (max-width: $--breakpoint-xs) {
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-dialog__close {
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
color: var(--color-text-xlight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -161,6 +161,7 @@ export default mixins(
|
||||||
|
|
||||||
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
|
this.$emit('workflowActiveChanged', { id: this.workflowId, active: newActiveState });
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
|
this.$store.dispatch('settings/fetchPromptsData');
|
||||||
},
|
},
|
||||||
async displayActivationError () {
|
async displayActivationError () {
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
|
|
|
@ -26,6 +26,8 @@ export const CREDENTIAL_EDIT_MODAL_KEY = 'editCredential';
|
||||||
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
|
export const CREDENTIAL_SELECT_MODAL_KEY = 'selectCredential';
|
||||||
export const CREDENTIAL_LIST_MODAL_KEY = 'credentialsList';
|
export const CREDENTIAL_LIST_MODAL_KEY = 'credentialsList';
|
||||||
export const PERSONALIZATION_MODAL_KEY = 'personalization';
|
export const PERSONALIZATION_MODAL_KEY = 'personalization';
|
||||||
|
export const CONTACT_PROMPT_MODAL_KEY = 'contactPrompt';
|
||||||
|
export const VALUE_SURVEY_MODAL_KEY = 'valueSurvey';
|
||||||
|
|
||||||
// breakpoints
|
// breakpoints
|
||||||
export const BREAKPOINT_SM = 768;
|
export const BREAKPOINT_SM = 768;
|
||||||
|
@ -132,4 +134,5 @@ export const CODING_SKILL_KEY = 'codingSkill';
|
||||||
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
export const OTHER_WORK_AREA_KEY = 'otherWorkArea';
|
||||||
export const OTHER_COMPANY_INDUSTRY_KEY = 'otherCompanyIndustry';
|
export const OTHER_COMPANY_INDUSTRY_KEY = 'otherCompanyIndustry';
|
||||||
|
|
||||||
export const VALID_EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
export const VALID_EMAIL_REGEX = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,17 @@ export function getPersonalizedNodeTypes(answers: IPersonalizationSurveyAnswers)
|
||||||
const companySize = answers[COMPANY_SIZE_KEY];
|
const companySize = answers[COMPANY_SIZE_KEY];
|
||||||
const workArea = answers[WORK_AREA_KEY];
|
const workArea = answers[WORK_AREA_KEY];
|
||||||
|
|
||||||
if (companySize === null && workArea === null && answers[CODING_SKILL_KEY] === null) {
|
function isWorkAreaAnswer(name: string) {
|
||||||
|
if (Array.isArray(workArea)) {
|
||||||
|
return workArea.includes(name);
|
||||||
|
} else {
|
||||||
|
return workArea === name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const workAreaIsEmpty = workArea === null || workArea.length === 0;
|
||||||
|
|
||||||
|
if (companySize === null && workAreaIsEmpty && answers[CODING_SKILL_KEY] === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +27,7 @@ export function getPersonalizedNodeTypes(answers: IPersonalizationSurveyAnswers)
|
||||||
}
|
}
|
||||||
|
|
||||||
let nodeTypes = [] as string[];
|
let nodeTypes = [] as string[];
|
||||||
if (workArea === IT_ENGINEERING_WORK_AREA) {
|
if (isWorkAreaAnswer(IT_ENGINEERING_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE);
|
nodeTypes = nodeTypes.concat(WEBHOOK_NODE_TYPE);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -39,16 +49,16 @@ export function getPersonalizedNodeTypes(answers: IPersonalizationSurveyAnswers)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (companySize === COMPANY_SIZE_500_999 || companySize === COMPANY_SIZE_1000_OR_MORE) {
|
if (companySize === COMPANY_SIZE_500_999 || companySize === COMPANY_SIZE_1000_OR_MORE) {
|
||||||
if (workArea === SALES_BUSINESSDEV_WORK_AREA) {
|
if (isWorkAreaAnswer(SALES_BUSINESSDEV_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat(SALESFORCE_NODE_TYPE);
|
nodeTypes = nodeTypes.concat(SALESFORCE_NODE_TYPE);
|
||||||
}
|
}
|
||||||
else if (workArea === SECURITY_WORK_AREA) {
|
else if (isWorkAreaAnswer(SECURITY_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([ELASTIC_SECURITY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([ELASTIC_SECURITY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else if (workArea === PRODUCT_WORK_AREA) {
|
else if (isWorkAreaAnswer(PRODUCT_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, SEGMENT_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, SEGMENT_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else if (workArea === IT_ENGINEERING_WORK_AREA) {
|
else if (isWorkAreaAnswer(IT_ENGINEERING_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([GITHUB_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([GITHUB_TRIGGER_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -56,19 +66,19 @@ export function getPersonalizedNodeTypes(answers: IPersonalizationSurveyAnswers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if (workArea === SALES_BUSINESSDEV_WORK_AREA) {
|
if (isWorkAreaAnswer(SALES_BUSINESSDEV_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat(CLEARBIT_NODE_TYPE);
|
nodeTypes = nodeTypes.concat(CLEARBIT_NODE_TYPE);
|
||||||
}
|
}
|
||||||
else if (workArea === SECURITY_WORK_AREA) {
|
else if (isWorkAreaAnswer(SECURITY_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([PAGERDUTY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([PAGERDUTY_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else if (workArea === PRODUCT_WORK_AREA) {
|
else if (isWorkAreaAnswer(PRODUCT_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, CALENDLY_TRIGGER_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([JIRA_TRIGGER_NODE_TYPE, CALENDLY_TRIGGER_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else if (workArea === IT_ENGINEERING_WORK_AREA) {
|
else if (isWorkAreaAnswer(IT_ENGINEERING_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([EXECUTE_COMMAND_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([EXECUTE_COMMAND_NODE_TYPE, HTTP_REQUEST_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else if (workArea === FINANCE_WORK_AREA) {
|
else if (isWorkAreaAnswer(FINANCE_WORK_AREA)) {
|
||||||
nodeTypes = nodeTypes.concat([XERO_NODE_TYPE, QUICKBOOKS_NODE_TYPE, SPREADSHEET_FILE_NODE_TYPE]);
|
nodeTypes = nodeTypes.concat([XERO_NODE_TYPE, QUICKBOOKS_NODE_TYPE, SPREADSHEET_FILE_NODE_TYPE]);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import { ActionContext, Module } from 'vuex';
|
import { ActionContext, Module } from 'vuex';
|
||||||
import {
|
import {
|
||||||
|
IN8nPrompts,
|
||||||
IN8nUISettings,
|
IN8nUISettings,
|
||||||
|
IN8nValueSurveyData,
|
||||||
IPersonalizationSurveyAnswers,
|
IPersonalizationSurveyAnswers,
|
||||||
IRootState,
|
IRootState,
|
||||||
ISettingsState,
|
ISettingsState,
|
||||||
} from '../Interface';
|
} from '../Interface';
|
||||||
import { getSettings, submitPersonalizationSurvey } from '../api/settings';
|
import { getPromptsData, getSettings, submitValueSurvey, submitPersonalizationSurvey, submitContactInfo } from '../api/settings';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { getPersonalizedNodeTypes } from './helper';
|
import { getPersonalizedNodeTypes } from './helper';
|
||||||
import { PERSONALIZATION_MODAL_KEY } from '@/constants';
|
import { CONTACT_PROMPT_MODAL_KEY, PERSONALIZATION_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||||
|
|
||||||
const module: Module<ISettingsState, IRootState> = {
|
const module: Module<ISettingsState, IRootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
settings: {} as IN8nUISettings,
|
settings: {} as IN8nUISettings,
|
||||||
|
promptsData: {} as IN8nPrompts,
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
personalizedNodeTypes(state: ISettingsState): string[] {
|
personalizedNodeTypes(state: ISettingsState): string[] {
|
||||||
|
@ -24,6 +27,9 @@ const module: Module<ISettingsState, IRootState> = {
|
||||||
|
|
||||||
return getPersonalizedNodeTypes(answers);
|
return getPersonalizedNodeTypes(answers);
|
||||||
},
|
},
|
||||||
|
getPromptsData(state: ISettingsState) {
|
||||||
|
return state.promptsData;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
setSettings(state: ISettingsState, settings: IN8nUISettings) {
|
setSettings(state: ISettingsState, settings: IN8nUISettings) {
|
||||||
|
@ -35,6 +41,9 @@ const module: Module<ISettingsState, IRootState> = {
|
||||||
shouldShow: false,
|
shouldShow: false,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
setPromptsData(state: ISettingsState, promptsData: IN8nPrompts) {
|
||||||
|
Vue.set(state, 'promptsData', promptsData);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
actions: {
|
actions: {
|
||||||
async getSettings(context: ActionContext<ISettingsState, IRootState>) {
|
async getSettings(context: ActionContext<ISettingsState, IRootState>) {
|
||||||
|
@ -71,6 +80,40 @@ const module: Module<ISettingsState, IRootState> = {
|
||||||
|
|
||||||
context.commit('setPersonalizationAnswers', results);
|
context.commit('setPersonalizationAnswers', results);
|
||||||
},
|
},
|
||||||
|
async fetchPromptsData(context: ActionContext<ISettingsState, IRootState>) {
|
||||||
|
if (!context.rootGetters.isTelemetryEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const promptsData: IN8nPrompts = await getPromptsData(context.state.settings.instanceId);
|
||||||
|
|
||||||
|
if (promptsData && promptsData.showContactPrompt) {
|
||||||
|
context.commit('ui/openModal', CONTACT_PROMPT_MODAL_KEY, {root: true});
|
||||||
|
} else if (promptsData && promptsData.showValueSurvey) {
|
||||||
|
context.commit('ui/openModal', VALUE_SURVEY_MODAL_KEY, {root: true});
|
||||||
|
}
|
||||||
|
|
||||||
|
context.commit('setPromptsData', promptsData);
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
async submitContactInfo(context: ActionContext<ISettingsState, IRootState>, email: string) {
|
||||||
|
try {
|
||||||
|
return await submitContactInfo(context.state.settings.instanceId, email);
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async submitValueSurvey(context: ActionContext<ISettingsState, IRootState>, params: IN8nValueSurveyData) {
|
||||||
|
try {
|
||||||
|
return await submitValueSurvey(context.state.settings.instanceId, params);
|
||||||
|
} catch (e) {
|
||||||
|
return e;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, PERSONALIZATION_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY } from '@/constants';
|
import { CONTACT_PROMPT_MODAL_KEY, CREDENTIAL_EDIT_MODAL_KEY, DUPLICATE_MODAL_KEY, PERSONALIZATION_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, CREDENTIAL_LIST_MODAL_KEY, VALUE_SURVEY_MODAL_KEY } from '@/constants';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { ActionContext, Module } from 'vuex';
|
import { ActionContext, Module } from 'vuex';
|
||||||
import {
|
import {
|
||||||
|
@ -10,6 +10,9 @@ const module: Module<IUiState, IRootState> = {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {
|
||||||
modals: {
|
modals: {
|
||||||
|
[CONTACT_PROMPT_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
[CREDENTIAL_EDIT_MODAL_KEY]: {
|
[CREDENTIAL_EDIT_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
mode: '',
|
mode: '',
|
||||||
|
@ -33,6 +36,9 @@ const module: Module<IUiState, IRootState> = {
|
||||||
[WORKFLOW_OPEN_MODAL_KEY]: {
|
[WORKFLOW_OPEN_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
[VALUE_SURVEY_MODAL_KEY]: {
|
||||||
|
open: false,
|
||||||
|
},
|
||||||
[VERSIONS_MODAL_KEY]: {
|
[VERSIONS_MODAL_KEY]: {
|
||||||
open: false,
|
open: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -55,6 +55,7 @@ import {
|
||||||
N8nMenuItem,
|
N8nMenuItem,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nSquareButton,
|
||||||
N8nText,
|
N8nText,
|
||||||
N8nTooltip,
|
N8nTooltip,
|
||||||
N8nOption,
|
N8nOption,
|
||||||
|
@ -75,6 +76,7 @@ Vue.use(N8nMenu);
|
||||||
Vue.use(N8nMenuItem);
|
Vue.use(N8nMenuItem);
|
||||||
Vue.use(N8nSelect);
|
Vue.use(N8nSelect);
|
||||||
Vue.use(N8nSpinner);
|
Vue.use(N8nSpinner);
|
||||||
|
Vue.component('n8n-square-button', N8nSquareButton);
|
||||||
Vue.component('n8n-text', N8nText);
|
Vue.component('n8n-text', N8nText);
|
||||||
Vue.use(N8nTooltip);
|
Vue.use(N8nTooltip);
|
||||||
Vue.use(N8nOption);
|
Vue.use(N8nOption);
|
||||||
|
|
|
@ -66,6 +66,12 @@ class Telemetry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
page(category?: string, name?: string | undefined | null) {
|
||||||
|
if (this.telemetry) {
|
||||||
|
this.telemetry.page(category, name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
trackNodesPanel(event: string, properties: IDataObject = {}) {
|
trackNodesPanel(event: string, properties: IDataObject = {}) {
|
||||||
if (this.telemetry) {
|
if (this.telemetry) {
|
||||||
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
|
properties.nodes_panel_session_id = this.userNodesPanelSession.sessionId;
|
||||||
|
|
|
@ -656,6 +656,10 @@ export const store = new Vuex.Store({
|
||||||
return state.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID;
|
return state.workflow.id === PLACEHOLDER_EMPTY_WORKFLOW_ID;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isTelemetryEnabled: (state) => {
|
||||||
|
return state.telemetry && state.telemetry.enabled;
|
||||||
|
},
|
||||||
|
|
||||||
currentWorkflowHasWebhookNode: (state: IRootState): boolean => {
|
currentWorkflowHasWebhookNode: (state: IRootState): boolean => {
|
||||||
return !!state.workflow.nodes.find((node: INodeUi) => !!node.webhookId);
|
return !!state.workflow.nodes.find((node: INodeUi) => !!node.webhookId);
|
||||||
},
|
},
|
||||||
|
|
|
@ -755,7 +755,7 @@ export default mixins(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.callDebounced('saveCurrentWorkflow', 1000, undefined, true);
|
this.callDebounced('onSaveKeyboardShortcut', 1000);
|
||||||
} else if (e.key === 'Enter') {
|
} else if (e.key === 'Enter') {
|
||||||
// Activate the last selected node
|
// Activate the last selected node
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
|
@ -954,15 +954,22 @@ export default mixins(
|
||||||
},
|
},
|
||||||
|
|
||||||
cutSelectedNodes () {
|
cutSelectedNodes () {
|
||||||
this.copySelectedNodes();
|
this.copySelectedNodes(true);
|
||||||
this.deleteSelectedNodes();
|
this.deleteSelectedNodes();
|
||||||
},
|
},
|
||||||
|
|
||||||
copySelectedNodes () {
|
copySelectedNodes (isCut: boolean) {
|
||||||
this.getSelectedNodesToSave().then((data) => {
|
this.getSelectedNodesToSave().then((data) => {
|
||||||
const nodeData = JSON.stringify(data, null, 2);
|
const nodeData = JSON.stringify(data, null, 2);
|
||||||
this.copyToClipboard(nodeData);
|
this.copyToClipboard(nodeData);
|
||||||
if (data.nodes.length > 0) {
|
if (data.nodes.length > 0) {
|
||||||
|
if(!isCut){
|
||||||
|
this.$showMessage({
|
||||||
|
title: 'Copied!',
|
||||||
|
message: '',
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
}
|
||||||
this.$telemetry.track('User copied nodes', {
|
this.$telemetry.track('User copied nodes', {
|
||||||
node_types: data.nodes.map((node) => node.type),
|
node_types: data.nodes.map((node) => node.type),
|
||||||
workflow_id: this.$store.getters.workflowId,
|
workflow_id: this.$store.getters.workflowId,
|
||||||
|
@ -2732,6 +2739,7 @@ export default mixins(
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$externalHooks().run('nodeView.mount');
|
this.$externalHooks().run('nodeView.mount');
|
||||||
|
this.$telemetry.page('Editor', this.$route.name);
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyed () {
|
destroyed () {
|
||||||
|
|
18
packages/nodes-base/credentials/FigmaApi.credentials.ts
Normal file
18
packages/nodes-base/credentials/FigmaApi.credentials.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class FigmaApi implements ICredentialType {
|
||||||
|
name = 'figmaApi';
|
||||||
|
displayName = 'Figma API';
|
||||||
|
documentationUrl = 'figma';
|
||||||
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Access Token',
|
||||||
|
name: 'accessToken',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -4,7 +4,12 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
const scopes = [
|
const scopes = [
|
||||||
'contacts',
|
'crm.objects.contacts.read',
|
||||||
|
'crm.schemas.contacts.read',
|
||||||
|
'crm.objects.companies.read',
|
||||||
|
'crm.schemas.companies.read',
|
||||||
|
'crm.objects.deals.read',
|
||||||
|
'crm.schemas.deals.read',
|
||||||
];
|
];
|
||||||
|
|
||||||
export class HubspotDeveloperApi implements ICredentialType {
|
export class HubspotDeveloperApi implements ICredentialType {
|
||||||
|
|
|
@ -4,7 +4,16 @@ import {
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
const scopes = [
|
const scopes = [
|
||||||
'contacts',
|
'crm.schemas.deals.read',
|
||||||
|
'crm.objects.owners.read',
|
||||||
|
'crm.objects.contacts.write',
|
||||||
|
'crm.objects.companies.write',
|
||||||
|
'crm.objects.companies.read',
|
||||||
|
'crm.objects.deals.read',
|
||||||
|
'crm.schemas.contacts.read',
|
||||||
|
'crm.objects.deals.write',
|
||||||
|
'crm.objects.contacts.read',
|
||||||
|
'crm.schemas.companies.read',
|
||||||
'forms',
|
'forms',
|
||||||
'tickets',
|
'tickets',
|
||||||
];
|
];
|
||||||
|
|
|
@ -33,7 +33,7 @@ export class PagerDutyOAuth2Api implements ICredentialType {
|
||||||
displayName: 'Scope',
|
displayName: 'Scope',
|
||||||
name: 'scope',
|
name: 'scope',
|
||||||
type: 'hidden',
|
type: 'hidden',
|
||||||
default: '',
|
default: 'write',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Authentication',
|
displayName: 'Authentication',
|
||||||
|
|
24
packages/nodes-base/credentials/WorkableApi.credentials.ts
Normal file
24
packages/nodes-base/credentials/WorkableApi.credentials.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import {
|
||||||
|
ICredentialType,
|
||||||
|
INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class WorkableApi implements ICredentialType {
|
||||||
|
name = 'workableApi';
|
||||||
|
displayName = 'Workable API';
|
||||||
|
documentationUrl = 'workable';
|
||||||
|
properties: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Subdomain',
|
||||||
|
name: 'subdomain',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Access Token',
|
||||||
|
name: 'accessToken',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
|
@ -452,7 +452,10 @@ export class ApiTemplateIo implements INodeType {
|
||||||
try {
|
try {
|
||||||
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
|
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
|
||||||
|
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
let options: IDataObject = {};
|
||||||
|
if (download) {
|
||||||
|
options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
}
|
||||||
|
|
||||||
const qs = {
|
const qs = {
|
||||||
template_id: this.getNodeParameter('imageTemplateId', i),
|
template_id: this.getNodeParameter('imageTemplateId', i),
|
||||||
|
@ -529,7 +532,10 @@ export class ApiTemplateIo implements INodeType {
|
||||||
try {
|
try {
|
||||||
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
|
const jsonParameters = this.getNodeParameter('jsonParameters', i) as boolean;
|
||||||
|
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
let options: IDataObject = {};
|
||||||
|
if (download) {
|
||||||
|
options = this.getNodeParameter('options', i) as IDataObject;
|
||||||
|
}
|
||||||
|
|
||||||
const qs = {
|
const qs = {
|
||||||
template_id: this.getNodeParameter('pdfTemplateId', i),
|
template_id: this.getNodeParameter('pdfTemplateId', i),
|
||||||
|
|
183
packages/nodes-base/nodes/Figma/FigmaTrigger.node.ts
Normal file
183
packages/nodes-base/nodes/Figma/FigmaTrigger.node.ts
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
import {
|
||||||
|
IHookFunctions,
|
||||||
|
IWebhookFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IWebhookResponseData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
figmaApiRequest,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
snakeCase,
|
||||||
|
} from 'change-case';
|
||||||
|
|
||||||
|
import {
|
||||||
|
randomBytes,
|
||||||
|
} from 'crypto';
|
||||||
|
|
||||||
|
export class FigmaTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Figma Trigger (Beta)',
|
||||||
|
name: 'figmaTrigger',
|
||||||
|
icon: 'file:figma.svg',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["triggerOn"]}}',
|
||||||
|
description: 'Starts the workflow when Figma events occur',
|
||||||
|
defaults: {
|
||||||
|
name: 'Figma Trigger (Beta)',
|
||||||
|
color: '#29b6f6',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'figmaApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webhooks: [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
path: 'webhook',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Team ID',
|
||||||
|
name: 'teamId',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: '',
|
||||||
|
description: 'Trigger will monitor this Figma Team for changes. Team ID can be found in the URL of a Figma Team page when viewed in a web browser: figma.com/files/team/{TEAM-ID}/',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Trigger On',
|
||||||
|
name: 'triggerOn',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'File Commented',
|
||||||
|
value: 'fileComment',
|
||||||
|
description: 'Triggers when someone comments on a file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'File Deleted',
|
||||||
|
value: 'fileDelete',
|
||||||
|
description: 'Triggers whenever a file has been deleted. Does not trigger on all files within a folder, if the folder is deleted',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'File Updated',
|
||||||
|
value: 'fileUpdate',
|
||||||
|
description: 'Triggers whenever a file saves or is deleted. This occurs whenever a file is closed or within 30 seconds after changes have been made',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'File Version Updated',
|
||||||
|
value: 'fileVersionUpdate',
|
||||||
|
description: 'Triggers whenever a named version is created in the version history of a file',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Library Publish',
|
||||||
|
value: 'libraryPublish',
|
||||||
|
description: 'Triggers whenever a library file is published',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore (because of request)
|
||||||
|
webhookMethods = {
|
||||||
|
default: {
|
||||||
|
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
const teamId = this.getNodeParameter('teamId') as string;
|
||||||
|
const triggerOn = this.getNodeParameter('triggerOn') as string;
|
||||||
|
// Check all the webhooks which exist already if it is identical to the
|
||||||
|
// one that is supposed to get created.
|
||||||
|
const { webhooks } = await figmaApiRequest.call(this, 'GET', `/v2/teams/${teamId}/webhooks`);
|
||||||
|
for (const webhook of webhooks) {
|
||||||
|
if (webhook.endpoint === webhookUrl
|
||||||
|
&& webhook.team_id === teamId
|
||||||
|
&& webhook.event_type === snakeCase(triggerOn).toUpperCase()
|
||||||
|
&& webhook.status === 'ACTIVE') {
|
||||||
|
webhookData.webhookId = webhook.id as string;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
async create(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||||
|
const triggerOn = this.getNodeParameter('triggerOn') as string;
|
||||||
|
const teamId = this.getNodeParameter('teamId') as string;
|
||||||
|
const endpoint = '/v2/webhooks';
|
||||||
|
|
||||||
|
const body: IDataObject = {
|
||||||
|
event_type: snakeCase(triggerOn).toUpperCase(),
|
||||||
|
team_id: teamId,
|
||||||
|
description: `n8n-webhook:${webhookUrl}`,
|
||||||
|
endpoint: webhookUrl,
|
||||||
|
passcode: randomBytes(10).toString('hex') as string,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData = await figmaApiRequest.call(this, 'POST', endpoint, body);
|
||||||
|
|
||||||
|
if (responseData.id === undefined) {
|
||||||
|
// Required data is missing so was not successful
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookData.webhookId = responseData.id as string;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async delete(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
if (webhookData.webhookId !== undefined) {
|
||||||
|
|
||||||
|
const endpoint = `/v2/webhooks/${webhookData.webhookId}`;
|
||||||
|
try {
|
||||||
|
await figmaApiRequest.call(this, 'DELETE', endpoint);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Remove from the static workflow data so that it is clear
|
||||||
|
// that no webhooks are registred anymore
|
||||||
|
delete webhookData.webhookId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||||
|
const bodyData = this.getBodyData();
|
||||||
|
|
||||||
|
if (bodyData.event_type === 'PING') {
|
||||||
|
const res = this.getResponseObject();
|
||||||
|
res.status(200).end();
|
||||||
|
return {
|
||||||
|
noWebhookResponse: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
workflowData: [
|
||||||
|
this.helpers.returnJsonArray(bodyData),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
36
packages/nodes-base/nodes/Figma/GenericFunctions.ts
Normal file
36
packages/nodes-base/nodes/Figma/GenericFunctions.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import {
|
||||||
|
OptionsWithUri,
|
||||||
|
} from 'request';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
IExecuteSingleFunctions,
|
||||||
|
IHookFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
NodeApiError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function figmaApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||||
|
const credentials = await this.getCredentials('figmaApi') as { accessToken: string };
|
||||||
|
|
||||||
|
let options: OptionsWithUri = {
|
||||||
|
headers: { 'X-FIGMA-TOKEN': credentials.accessToken },
|
||||||
|
method,
|
||||||
|
body,
|
||||||
|
uri: uri || `https://api.figma.com${resource}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
options = Object.assign({}, options, option);
|
||||||
|
if (Object.keys(options.body).length === 0) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.helpers.request!(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
1
packages/nodes-base/nodes/Figma/figma.svg
Normal file
1
packages/nodes-base/nodes/Figma/figma.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 300" width="1667" height="2500"><style type="text/css">.st0{fill:#0acf83}.st1{fill:#a259ff}.st2{fill:#f24e1e}.st3{fill:#ff7262}.st4{fill:#1abcfe}</style><title>Figma.logo</title><desc>Created using Figma</desc><path id="path0_fill" class="st0" d="M50 300c27.6 0 50-22.4 50-50v-50H50c-27.6 0-50 22.4-50 50s22.4 50 50 50z"/><path id="path1_fill" class="st1" d="M0 150c0-27.6 22.4-50 50-50h50v100H50c-27.6 0-50-22.4-50-50z"/><path id="path1_fill_1_" class="st2" d="M0 50C0 22.4 22.4 0 50 0h50v100H50C22.4 100 0 77.6 0 50z"/><path id="path2_fill" class="st3" d="M100 0h50c27.6 0 50 22.4 50 50s-22.4 50-50 50h-50V0z"/><path id="path3_fill" class="st4" d="M200 150c0 27.6-22.4 50-50 50s-50-22.4-50-50 22.4-50 50-50 50 22.4 50 50z"/></svg>
|
After Width: | Height: | Size: 802 B |
|
@ -887,6 +887,43 @@ export const contactFields: INodeProperties[] = [
|
||||||
default: '',
|
default: '',
|
||||||
description: 'A field mask to restrict which fields on each person are returned. Multiple fields can be specified by separating them with commas.',
|
description: 'A field mask to restrict which fields on each person are returned. Multiple fields can be specified by separating them with commas.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Use Query',
|
||||||
|
name: 'useQuery',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'contact',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
description: `Whether or not to use a query to filter the results`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Query',
|
||||||
|
name: 'query',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'getAll',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'contact',
|
||||||
|
],
|
||||||
|
useQuery: [
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `The plain-text query for the request. The query is used to match prefix phrases of the fields on a person. For example, a person with name "foo name" matches queries such as "f", "fo", "foo", "foo n", "nam", etc., but not "oo n".`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
displayName: 'RAW Data',
|
displayName: 'RAW Data',
|
||||||
name: 'rawData',
|
name: 'rawData',
|
||||||
|
@ -918,6 +955,9 @@ export const contactFields: INodeProperties[] = [
|
||||||
resource: [
|
resource: [
|
||||||
'contact',
|
'contact',
|
||||||
],
|
],
|
||||||
|
useQuery: [
|
||||||
|
false,
|
||||||
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
options: [
|
options: [
|
||||||
|
|
|
@ -24,6 +24,7 @@ import {
|
||||||
} from './ContactDescription';
|
} from './ContactDescription';
|
||||||
|
|
||||||
import * as moment from 'moment';
|
import * as moment from 'moment';
|
||||||
|
import { IData } from '../Analytics/Interfaces';
|
||||||
|
|
||||||
export class GoogleContacts implements INodeType {
|
export class GoogleContacts implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
@ -264,11 +265,19 @@ export class GoogleContacts implements INodeType {
|
||||||
responseData.contactId = responseData.resourceName.split('/')[1];
|
responseData.contactId = responseData.resourceName.split('/')[1];
|
||||||
}
|
}
|
||||||
//https://developers.google.com/people/api/rest/v1/people.connections/list
|
//https://developers.google.com/people/api/rest/v1/people.connections/list
|
||||||
|
//https://developers.google.com/people/api/rest/v1/people/searchContacts
|
||||||
if (operation === 'getAll') {
|
if (operation === 'getAll') {
|
||||||
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
|
||||||
const fields = this.getNodeParameter('fields', i) as string[];
|
const fields = this.getNodeParameter('fields', i) as string[];
|
||||||
const options = this.getNodeParameter('options', i) as IDataObject;
|
const options = this.getNodeParameter('options', i, {}) as IDataObject;
|
||||||
const rawData = this.getNodeParameter('rawData', i) as boolean;
|
const rawData = this.getNodeParameter('rawData', i) as boolean;
|
||||||
|
const useQuery = this.getNodeParameter('useQuery', i) as boolean;
|
||||||
|
|
||||||
|
const endpoint = (useQuery) ? ':searchContacts' : '/me/connections';
|
||||||
|
|
||||||
|
if (useQuery) {
|
||||||
|
qs.query = this.getNodeParameter('query', i) as string;
|
||||||
|
}
|
||||||
|
|
||||||
if (options.sortOrder) {
|
if (options.sortOrder) {
|
||||||
qs.sortOrder = options.sortOrder as number;
|
qs.sortOrder = options.sortOrder as number;
|
||||||
|
@ -280,25 +289,36 @@ export class GoogleContacts implements INodeType {
|
||||||
qs.personFields = (fields as string[]).join(',');
|
qs.personFields = (fields as string[]).join(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useQuery) {
|
||||||
|
qs.readMask = qs.personFields;
|
||||||
|
delete qs.personFields;
|
||||||
|
}
|
||||||
|
|
||||||
if (returnAll) {
|
if (returnAll) {
|
||||||
responseData = await googleApiRequestAllItems.call(
|
responseData = await googleApiRequestAllItems.call(
|
||||||
this,
|
this,
|
||||||
'connections',
|
(useQuery) ? 'results' : 'connections',
|
||||||
'GET',
|
'GET',
|
||||||
`/people/me/connections`,
|
`/people${endpoint}`,
|
||||||
{},
|
{},
|
||||||
qs,
|
qs,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (useQuery) {
|
||||||
|
responseData = responseData.map((result: IDataObject) => result.person);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
qs.pageSize = this.getNodeParameter('limit', i) as number;
|
qs.pageSize = this.getNodeParameter('limit', i) as number;
|
||||||
responseData = await googleApiRequest.call(
|
responseData = await googleApiRequest.call(
|
||||||
this,
|
this,
|
||||||
'GET',
|
'GET',
|
||||||
`/people/me/connections`,
|
`/people${endpoint}`,
|
||||||
{},
|
{},
|
||||||
qs,
|
qs,
|
||||||
);
|
);
|
||||||
responseData = responseData.connections;
|
|
||||||
|
responseData = responseData.connections || responseData.results?.map((result: IDataObject) => result.person) || [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!rawData) {
|
if (!rawData) {
|
||||||
|
|
|
@ -33,7 +33,10 @@ export const cameraProxyFields: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Camera Entity ID',
|
displayName: 'Camera Entity ID',
|
||||||
name: 'cameraEntityId',
|
name: 'cameraEntityId',
|
||||||
type: 'string',
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getCameraEntities',
|
||||||
|
},
|
||||||
default: '',
|
default: '',
|
||||||
required: true,
|
required: true,
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
|
|
@ -4,15 +4,17 @@ import {
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IExecuteFunctions,
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
} from 'n8n-core';
|
} from 'n8n-core';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
INodePropertyOptions,
|
||||||
NodeApiError,
|
NodeApiError,
|
||||||
NodeOperationError,
|
NodeOperationError,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
export async function homeAssistantApiRequest(this: IExecuteFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) {
|
export async function homeAssistantApiRequest(this: IExecuteFunctions | ILoadOptionsFunctions, method: string, resource: string, body: IDataObject = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}) {
|
||||||
const credentials = await this.getCredentials('homeAssistantApi');
|
const credentials = await this.getCredentials('homeAssistantApi');
|
||||||
|
|
||||||
if (credentials === undefined) {
|
if (credentials === undefined) {
|
||||||
|
@ -35,8 +37,51 @@ export async function homeAssistantApiRequest(this: IExecuteFunctions, method: s
|
||||||
delete options.body;
|
delete options.body;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return await this.helpers.request(options);
|
if (this.helpers.request) {
|
||||||
|
return await this.helpers.request(options);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new NodeApiError(this.getNode(), error);
|
throw new NodeApiError(this.getNode(), error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getHomeAssistantEntities(this: IExecuteFunctions | ILoadOptionsFunctions, domain = '') {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const entities = await homeAssistantApiRequest.call(this, 'GET', '/states');
|
||||||
|
for (const entity of entities) {
|
||||||
|
const entityId = entity.entity_id as string;
|
||||||
|
if (domain === '' || domain && entityId.startsWith(domain)) {
|
||||||
|
const entityName = entity.attributes.friendly_name as string || entityId;
|
||||||
|
returnData.push({
|
||||||
|
name: entityName,
|
||||||
|
value: entityId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getHomeAssistantServices(this: IExecuteFunctions | ILoadOptionsFunctions, domain = '') {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const services = await homeAssistantApiRequest.call(this, 'GET', '/services');
|
||||||
|
if (domain === '') {
|
||||||
|
// If no domain specified return domains
|
||||||
|
const domains = services.map(({ domain }: IDataObject) => domain as string).sort();
|
||||||
|
returnData.push(...domains.map((service: string) => ({ name: service, value: service })));
|
||||||
|
return returnData;
|
||||||
|
} else {
|
||||||
|
// If we have a domain, return all relevant services
|
||||||
|
const domainServices = services.filter((service: IDataObject) => service.domain === domain);
|
||||||
|
for (const domainService of domainServices) {
|
||||||
|
for (const [serviceID, value] of Object.entries(domainService.services)) {
|
||||||
|
const serviceProperties = value as IDataObject;
|
||||||
|
const serviceName = serviceProperties.description || serviceID;
|
||||||
|
returnData.push({
|
||||||
|
name: serviceName as string,
|
||||||
|
value: serviceID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,9 @@ import {
|
||||||
ICredentialsDecrypted,
|
ICredentialsDecrypted,
|
||||||
ICredentialTestFunctions,
|
ICredentialTestFunctions,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
INodePropertyOptions,
|
||||||
INodeType,
|
INodeType,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
NodeCredentialTestResult,
|
NodeCredentialTestResult,
|
||||||
|
@ -52,6 +54,8 @@ import {
|
||||||
} from './CameraProxyDescription';
|
} from './CameraProxyDescription';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
getHomeAssistantEntities,
|
||||||
|
getHomeAssistantServices,
|
||||||
homeAssistantApiRequest,
|
homeAssistantApiRequest,
|
||||||
} from './GenericFunctions';
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
@ -169,9 +173,29 @@ export class HomeAssistant implements INodeType {
|
||||||
status: 'OK',
|
status: 'OK',
|
||||||
message: 'Authentication successful!',
|
message: 'Authentication successful!',
|
||||||
};
|
};
|
||||||
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
loadOptions: {
|
||||||
|
async getAllEntities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
return await getHomeAssistantEntities.call(this);
|
||||||
|
},
|
||||||
|
async getCameraEntities(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
return await getHomeAssistantEntities.call(this, 'camera');
|
||||||
|
},
|
||||||
|
async getDomains(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
return await getHomeAssistantServices.call(this);
|
||||||
|
},
|
||||||
|
async getDomainServices(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const currentDomain = this.getCurrentNodeParameter('domain') as string;
|
||||||
|
if (currentDomain) {
|
||||||
|
return await getHomeAssistantServices.call(this, currentDomain);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
|
|
@ -83,7 +83,10 @@ export const serviceFields: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Domain',
|
displayName: 'Domain',
|
||||||
name: 'domain',
|
name: 'domain',
|
||||||
type: 'string',
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getDomains',
|
||||||
|
},
|
||||||
default: '',
|
default: '',
|
||||||
required: true,
|
required: true,
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -100,7 +103,13 @@ export const serviceFields: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Service',
|
displayName: 'Service',
|
||||||
name: 'service',
|
name: 'service',
|
||||||
type: 'string',
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'domain',
|
||||||
|
],
|
||||||
|
loadOptionsMethod: 'getDomainServices',
|
||||||
|
},
|
||||||
default: '',
|
default: '',
|
||||||
required: true,
|
required: true,
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
|
|
@ -43,7 +43,10 @@ export const stateFields: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Entity ID',
|
displayName: 'Entity ID',
|
||||||
name: 'entityId',
|
name: 'entityId',
|
||||||
type: 'string',
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getAllEntities',
|
||||||
|
},
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
operation: [
|
operation: [
|
||||||
|
@ -110,7 +113,10 @@ export const stateFields: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
displayName: 'Entity ID',
|
displayName: 'Entity ID',
|
||||||
name: 'entityId',
|
name: 'entityId',
|
||||||
type: 'string',
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getAllEntities',
|
||||||
|
},
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
show: {
|
show: {
|
||||||
operation: [
|
operation: [
|
||||||
|
|
|
@ -39,12 +39,16 @@ export async function hubspotApiRequest(this: IHookFunctions | IExecuteFunctions
|
||||||
|
|
||||||
return await this.helpers.request!(options);
|
return await this.helpers.request!(options);
|
||||||
} else if (authenticationMethod === 'developerApi') {
|
} else if (authenticationMethod === 'developerApi') {
|
||||||
const credentials = await this.getCredentials('hubspotDeveloperApi');
|
if (endpoint.includes('webhooks')) {
|
||||||
|
|
||||||
options.qs.hapikey = credentials!.apiKey as string;
|
const credentials = await this.getCredentials('hubspotDeveloperApi');
|
||||||
return await this.helpers.request!(options);
|
options.qs.hapikey = credentials!.apiKey as string;
|
||||||
|
return await this.helpers.request!(options);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
return await this.helpers.requestOAuth2!.call(this, 'hubspotDeveloperApi', options, { tokenType: 'Bearer', includeCredentialsOnRefreshOnBody: true });
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// @ts-ignore
|
|
||||||
return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, { tokenType: 'Bearer', includeCredentialsOnRefreshOnBody: true });
|
return await this.helpers.requestOAuth2!.call(this, 'hubspotOAuth2Api', options, { tokenType: 'Bearer', includeCredentialsOnRefreshOnBody: true });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -143,6 +143,9 @@ export class HubspotTrigger implements INodeType {
|
||||||
name: 'property',
|
name: 'property',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'contact.propertyChange',
|
||||||
|
],
|
||||||
loadOptionsMethod: 'getContactProperties',
|
loadOptionsMethod: 'getContactProperties',
|
||||||
},
|
},
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -160,6 +163,9 @@ export class HubspotTrigger implements INodeType {
|
||||||
name: 'property',
|
name: 'property',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'company.propertyChange',
|
||||||
|
],
|
||||||
loadOptionsMethod: 'getCompanyProperties',
|
loadOptionsMethod: 'getCompanyProperties',
|
||||||
},
|
},
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -177,6 +183,9 @@ export class HubspotTrigger implements INodeType {
|
||||||
name: 'property',
|
name: 'property',
|
||||||
type: 'options',
|
type: 'options',
|
||||||
typeOptions: {
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: [
|
||||||
|
'deal.propertyChange',
|
||||||
|
],
|
||||||
loadOptionsMethod: 'getDealProperties',
|
loadOptionsMethod: 'getDealProperties',
|
||||||
},
|
},
|
||||||
displayOptions: {
|
displayOptions: {
|
||||||
|
@ -220,51 +229,48 @@ export class HubspotTrigger implements INodeType {
|
||||||
// select them easily
|
// select them easily
|
||||||
async getContactProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
async getContactProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
const returnData: INodePropertyOptions[] = [];
|
const returnData: INodePropertyOptions[] = [];
|
||||||
for (const field of contactFields) {
|
const endpoint = '/properties/v2/contacts/properties';
|
||||||
|
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||||
|
for (const property of properties) {
|
||||||
|
const propertyName = property.label;
|
||||||
|
const propertyId = property.name;
|
||||||
returnData.push({
|
returnData.push({
|
||||||
name: capitalCase(field.label),
|
name: propertyName,
|
||||||
value: field.id,
|
value: propertyId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
returnData.sort((a, b) => {
|
|
||||||
if (a.name < b.name) { return -1; }
|
|
||||||
if (a.name > b.name) { return 1; }
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
// Get all the available companies to display them to user so that he can
|
// Get all the available companies to display them to user so that he can
|
||||||
// select them easily
|
// select them easily
|
||||||
async getCompanyProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
async getCompanyProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
const returnData: INodePropertyOptions[] = [];
|
const returnData: INodePropertyOptions[] = [];
|
||||||
for (const field of companyFields) {
|
const endpoint = '/properties/v2/companies/properties';
|
||||||
|
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||||
|
for (const property of properties) {
|
||||||
|
const propertyName = property.label;
|
||||||
|
const propertyId = property.name;
|
||||||
returnData.push({
|
returnData.push({
|
||||||
name: capitalCase(field.label),
|
name: propertyName,
|
||||||
value: field.id,
|
value: propertyId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
returnData.sort((a, b) => {
|
|
||||||
if (a.name < b.name) { return -1; }
|
|
||||||
if (a.name > b.name) { return 1; }
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
// Get all the available deals to display them to user so that he can
|
// Get all the available deals to display them to user so that he can
|
||||||
// select them easily
|
// select them easily
|
||||||
async getDealProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
async getDealProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
const returnData: INodePropertyOptions[] = [];
|
const returnData: INodePropertyOptions[] = [];
|
||||||
for (const field of dealFields) {
|
const endpoint = '/properties/v2/deals/properties';
|
||||||
|
const properties = await hubspotApiRequest.call(this, 'GET', endpoint, {});
|
||||||
|
for (const property of properties) {
|
||||||
|
const propertyName = property.label;
|
||||||
|
const propertyId = property.name;
|
||||||
returnData.push({
|
returnData.push({
|
||||||
name: capitalCase(field.label),
|
name: propertyName,
|
||||||
value: field.id,
|
value: propertyId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
returnData.sort((a, b) => {
|
|
||||||
if (a.name < b.name) { return -1; }
|
|
||||||
if (a.name > b.name) { return 1; }
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
return returnData;
|
return returnData;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -506,9 +506,15 @@ export class Jira implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (additionalFields.reporter) {
|
if (additionalFields.reporter) {
|
||||||
fields.reporter = {
|
if (jiraVersion === 'server') {
|
||||||
id: additionalFields.reporter as string,
|
fields.reporter = {
|
||||||
};
|
name: additionalFields.reporter as string,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
fields.reporter = {
|
||||||
|
id: additionalFields.reporter as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (additionalFields.description) {
|
if (additionalFields.description) {
|
||||||
fields.description = additionalFields.description as string;
|
fields.description = additionalFields.description as string;
|
||||||
|
@ -586,9 +592,15 @@ export class Jira implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (updateFields.reporter) {
|
if (updateFields.reporter) {
|
||||||
fields.reporter = {
|
if (jiraVersion === 'server') {
|
||||||
id: updateFields.reporter as string,
|
fields.reporter = {
|
||||||
};
|
name: updateFields.reporter as string,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
fields.reporter = {
|
||||||
|
id: updateFields.reporter as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (updateFields.description) {
|
if (updateFields.description) {
|
||||||
fields.description = updateFields.description as string;
|
fields.description = updateFields.description as string;
|
||||||
|
|
|
@ -43,6 +43,10 @@ export class OneSimpleApi implements INodeType {
|
||||||
name: 'Information',
|
name: 'Information',
|
||||||
value: 'information',
|
value: 'information',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Social Profile',
|
||||||
|
value: 'socialProfile',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Utility',
|
name: 'Utility',
|
||||||
value: 'utility',
|
value: 'utility',
|
||||||
|
@ -55,7 +59,7 @@ export class OneSimpleApi implements INodeType {
|
||||||
default: 'website',
|
default: 'website',
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
// Generation
|
// website
|
||||||
{
|
{
|
||||||
displayName: 'Operation',
|
displayName: 'Operation',
|
||||||
name: 'operation',
|
name: 'operation',
|
||||||
|
@ -86,6 +90,32 @@ export class OneSimpleApi implements INodeType {
|
||||||
],
|
],
|
||||||
default: 'pdf',
|
default: 'pdf',
|
||||||
},
|
},
|
||||||
|
// socialProfile
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
resource: [
|
||||||
|
'socialProfile',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Instagram',
|
||||||
|
value: 'instagramProfile',
|
||||||
|
description: 'Get details about an Instagram profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Spotify',
|
||||||
|
value: 'spotifyArtistProfile',
|
||||||
|
description: 'Get details about a Spotify Artist',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'instagramProfile',
|
||||||
|
},
|
||||||
// Information
|
// Information
|
||||||
{
|
{
|
||||||
displayName: 'Operation',
|
displayName: 'Operation',
|
||||||
|
@ -113,7 +143,7 @@ export class OneSimpleApi implements INodeType {
|
||||||
default: 'exchangeRate',
|
default: 'exchangeRate',
|
||||||
description: 'The operation to perform.',
|
description: 'The operation to perform.',
|
||||||
},
|
},
|
||||||
// Utiliy
|
// Utility
|
||||||
{
|
{
|
||||||
displayName: 'Operation',
|
displayName: 'Operation',
|
||||||
name: 'operation',
|
name: 'operation',
|
||||||
|
@ -279,7 +309,7 @@ export class OneSimpleApi implements INodeType {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: `Normally the API will reuse a previously taken screenshot of the URL to give a faster response.
|
description: `Normally the API will reuse a previously taken screenshot of the URL to give a faster response.
|
||||||
This option allows you to retake the screenshot at that exact time, for those times when it's necessary`,
|
This option allows you to retake the screenshot at that exact time, for those times when it's necessary`,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -508,7 +538,7 @@ export class OneSimpleApi implements INodeType {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
default: false,
|
default: false,
|
||||||
description: `Normally the API will reuse a previously taken screenshot of the URL to give a faster response.
|
description: `Normally the API will reuse a previously taken screenshot of the URL to give a faster response.
|
||||||
This option allows you to retake the screenshot at that exact time, for those times when it's necessary`,
|
This option allows you to retake the screenshot at that exact time, for those times when it's necessary`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Full Page',
|
displayName: 'Full Page',
|
||||||
|
@ -519,6 +549,44 @@ export class OneSimpleApi implements INodeType {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// socialProfile: instagramProfile
|
||||||
|
{
|
||||||
|
displayName: 'Profile Name',
|
||||||
|
name: 'profileName',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'instagramProfile',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'socialProfile',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Profile name to get details of',
|
||||||
|
},
|
||||||
|
// socialProfile: spotifyArtistProfile
|
||||||
|
{
|
||||||
|
displayName: 'Artist Name',
|
||||||
|
name: 'artistName',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: [
|
||||||
|
'spotifyArtistProfile',
|
||||||
|
],
|
||||||
|
resource: [
|
||||||
|
'socialProfile',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'Artist name to get details for',
|
||||||
|
},
|
||||||
// information: exchangeRate
|
// information: exchangeRate
|
||||||
{
|
{
|
||||||
displayName: 'Value',
|
displayName: 'Value',
|
||||||
|
@ -778,6 +846,20 @@ export class OneSimpleApi implements INodeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resource === 'socialProfile') {
|
||||||
|
if (operation === 'instagramProfile') {
|
||||||
|
const profileName = this.getNodeParameter('profileName', i) as string;
|
||||||
|
qs.profile = profileName;
|
||||||
|
responseData = await oneSimpleApiRequest.call(this, 'GET', '/instagram_profile', {}, qs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation === 'spotifyArtistProfile') {
|
||||||
|
const artistName = this.getNodeParameter('artistName', i) as string;
|
||||||
|
qs.profile = artistName;
|
||||||
|
responseData = await oneSimpleApiRequest.call(this, 'GET', '/spotify_profile', {}, qs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (resource === 'information') {
|
if (resource === 'information') {
|
||||||
if (operation === 'exchangeRate') {
|
if (operation === 'exchangeRate') {
|
||||||
const value = this.getNodeParameter('value', i) as string;
|
const value = this.getNodeParameter('value', i) as string;
|
||||||
|
@ -865,3 +947,4 @@ export class OneSimpleApi implements INodeType {
|
||||||
return [this.helpers.returnJsonArray(returnData)];
|
return [this.helpers.returnJsonArray(returnData)];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1744,8 +1744,8 @@ export class Salesforce implements INodeType {
|
||||||
if (additionalFields.type !== undefined) {
|
if (additionalFields.type !== undefined) {
|
||||||
body.Type = additionalFields.type as string;
|
body.Type = additionalFields.type as string;
|
||||||
}
|
}
|
||||||
if (additionalFields.ammount !== undefined) {
|
if (additionalFields.amount !== undefined) {
|
||||||
body.Amount = additionalFields.ammount as number;
|
body.Amount = additionalFields.amount as number;
|
||||||
}
|
}
|
||||||
if (additionalFields.owner !== undefined) {
|
if (additionalFields.owner !== undefined) {
|
||||||
body.OwnerId = additionalFields.owner as string;
|
body.OwnerId = additionalFields.owner as string;
|
||||||
|
@ -1813,8 +1813,8 @@ export class Salesforce implements INodeType {
|
||||||
if (updateFields.type !== undefined) {
|
if (updateFields.type !== undefined) {
|
||||||
body.Type = updateFields.type as string;
|
body.Type = updateFields.type as string;
|
||||||
}
|
}
|
||||||
if (updateFields.ammount !== undefined) {
|
if (updateFields.amount !== undefined) {
|
||||||
body.Amount = updateFields.ammount as number;
|
body.Amount = updateFields.amount as number;
|
||||||
}
|
}
|
||||||
if (updateFields.owner !== undefined) {
|
if (updateFields.owner !== undefined) {
|
||||||
body.OwnerId = updateFields.owner as string;
|
body.OwnerId = updateFields.owner as string;
|
||||||
|
|
37
packages/nodes-base/nodes/Workable/GenericFunctions.ts
Normal file
37
packages/nodes-base/nodes/Workable/GenericFunctions.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import {
|
||||||
|
OptionsWithUri,
|
||||||
|
} from 'request';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IExecuteFunctions,
|
||||||
|
IExecuteSingleFunctions,
|
||||||
|
IHookFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
NodeApiError,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export async function workableApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
|
||||||
|
const credentials = await this.getCredentials('workableApi') as { accessToken: string, subdomain: string };
|
||||||
|
|
||||||
|
let options: OptionsWithUri = {
|
||||||
|
headers: { 'Authorization': `Bearer ${credentials.accessToken}` },
|
||||||
|
method,
|
||||||
|
qs,
|
||||||
|
body,
|
||||||
|
uri: uri || `https://${credentials.subdomain}.workable.com/spi/v3${resource}`,
|
||||||
|
json: true,
|
||||||
|
};
|
||||||
|
options = Object.assign({}, options, option);
|
||||||
|
if (Object.keys(options.body).length === 0) {
|
||||||
|
delete options.body;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return await this.helpers.request!(options);
|
||||||
|
} catch (error) {
|
||||||
|
throw new NodeApiError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
201
packages/nodes-base/nodes/Workable/WorkableTrigger.node.ts
Normal file
201
packages/nodes-base/nodes/Workable/WorkableTrigger.node.ts
Normal file
|
@ -0,0 +1,201 @@
|
||||||
|
import {
|
||||||
|
IHookFunctions,
|
||||||
|
IWebhookFunctions,
|
||||||
|
} from 'n8n-core';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IWebhookResponseData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
workableApiRequest,
|
||||||
|
} from './GenericFunctions';
|
||||||
|
|
||||||
|
import {
|
||||||
|
snakeCase,
|
||||||
|
} from 'change-case';
|
||||||
|
|
||||||
|
export class WorkableTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Workable Trigger',
|
||||||
|
name: 'workableTrigger',
|
||||||
|
icon: 'file:workable.png',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
subtitle: '={{$parameter["triggerOn"]}}',
|
||||||
|
description: 'Starts the workflow when Workable events occur',
|
||||||
|
defaults: {
|
||||||
|
name: 'Workable Trigger',
|
||||||
|
color: '#29b6f6',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: ['main'],
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'workableApi',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
webhooks: [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
path: 'webhook',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Trigger On',
|
||||||
|
name: 'triggerOn',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Candidate Created',
|
||||||
|
value: 'candidateCreated',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Candidate Moved',
|
||||||
|
value: 'candidateMoved',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: '',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Filters',
|
||||||
|
name: 'filters',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Filter',
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
displayName: 'Job',
|
||||||
|
name: 'job',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getJobs',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `Get notifications only for one job`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Stage',
|
||||||
|
name: 'stage',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsMethod: 'getStages',
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: `Get notifications for specific stages. e.g. 'hired'`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getJobs(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { jobs } = await workableApiRequest.call(this, 'GET', '/jobs');
|
||||||
|
for (const job of jobs) {
|
||||||
|
returnData.push({
|
||||||
|
name: job.full_title,
|
||||||
|
value: job.shortcode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return returnData;
|
||||||
|
},
|
||||||
|
async getStages(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const returnData: INodePropertyOptions[] = [];
|
||||||
|
const { stages } = await workableApiRequest.call(this, 'GET', '/stages');
|
||||||
|
for (const stage of stages) {
|
||||||
|
returnData.push({
|
||||||
|
name: stage.name,
|
||||||
|
value: stage.slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return returnData;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// @ts-ignore (because of request)
|
||||||
|
webhookMethods = {
|
||||||
|
default: {
|
||||||
|
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
// Check all the webhooks which exist already if it is identical to the
|
||||||
|
// one that is supposed to get created.
|
||||||
|
const { subscriptions } = await workableApiRequest.call(this, 'GET', `/subscriptions`);
|
||||||
|
for (const subscription of subscriptions) {
|
||||||
|
if (subscription.target === webhookUrl) {
|
||||||
|
webhookData.webhookId = subscription.id as string;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
async create(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const credentials = await this.getCredentials('workableApi') as { accessToken: string, subdomain: string };
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||||
|
const triggerOn = this.getNodeParameter('triggerOn') as string;
|
||||||
|
const { stage, job } = this.getNodeParameter('filters') as IDataObject;
|
||||||
|
const endpoint = '/subscriptions';
|
||||||
|
|
||||||
|
const body: IDataObject = {
|
||||||
|
event: snakeCase(triggerOn).toLowerCase(),
|
||||||
|
args: {
|
||||||
|
account_id: credentials.subdomain,
|
||||||
|
...(job) && { job_shortcode: job },
|
||||||
|
...(stage) && { stage_slug: stage },
|
||||||
|
},
|
||||||
|
target: webhookUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseData = await workableApiRequest.call(this, 'POST', endpoint, body);
|
||||||
|
|
||||||
|
if (responseData.id === undefined) {
|
||||||
|
// Required data is missing so was not successful
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
webhookData.webhookId = responseData.id as string;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
async delete(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
if (webhookData.webhookId !== undefined) {
|
||||||
|
|
||||||
|
const endpoint = `/subscriptions/${webhookData.webhookId}`;
|
||||||
|
try {
|
||||||
|
await workableApiRequest.call(this, 'DELETE', endpoint);
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Remove from the static workflow data so that it is clear
|
||||||
|
// that no webhooks are registred anymore
|
||||||
|
delete webhookData.webhookId;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||||
|
const bodyData = this.getBodyData();
|
||||||
|
return {
|
||||||
|
workflowData: [
|
||||||
|
this.helpers.returnJsonArray(bodyData),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
BIN
packages/nodes-base/nodes/Workable/workable.png
Normal file
BIN
packages/nodes-base/nodes/Workable/workable.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 766 B |
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "n8n-nodes-base",
|
"name": "n8n-nodes-base",
|
||||||
"version": "0.149.0",
|
"version": "0.150.0",
|
||||||
"description": "Base nodes of n8n",
|
"description": "Base nodes of n8n",
|
||||||
"license": "SEE LICENSE IN LICENSE.md",
|
"license": "SEE LICENSE IN LICENSE.md",
|
||||||
"homepage": "https://n8n.io",
|
"homepage": "https://n8n.io",
|
||||||
|
@ -89,6 +89,7 @@
|
||||||
"dist/credentials/EventbriteOAuth2Api.credentials.js",
|
"dist/credentials/EventbriteOAuth2Api.credentials.js",
|
||||||
"dist/credentials/FacebookGraphApi.credentials.js",
|
"dist/credentials/FacebookGraphApi.credentials.js",
|
||||||
"dist/credentials/FacebookGraphAppApi.credentials.js",
|
"dist/credentials/FacebookGraphAppApi.credentials.js",
|
||||||
|
"dist/credentials/FigmaApi.credentials.js",
|
||||||
"dist/credentials/FileMaker.credentials.js",
|
"dist/credentials/FileMaker.credentials.js",
|
||||||
"dist/credentials/FlowApi.credentials.js",
|
"dist/credentials/FlowApi.credentials.js",
|
||||||
"dist/credentials/FormIoApi.credentials.js",
|
"dist/credentials/FormIoApi.credentials.js",
|
||||||
|
@ -301,6 +302,7 @@
|
||||||
"dist/credentials/WiseApi.credentials.js",
|
"dist/credentials/WiseApi.credentials.js",
|
||||||
"dist/credentials/WooCommerceApi.credentials.js",
|
"dist/credentials/WooCommerceApi.credentials.js",
|
||||||
"dist/credentials/WordpressApi.credentials.js",
|
"dist/credentials/WordpressApi.credentials.js",
|
||||||
|
"dist/credentials/WorkableApi.credentials.js",
|
||||||
"dist/credentials/WufooApi.credentials.js",
|
"dist/credentials/WufooApi.credentials.js",
|
||||||
"dist/credentials/XeroOAuth2Api.credentials.js",
|
"dist/credentials/XeroOAuth2Api.credentials.js",
|
||||||
"dist/credentials/YourlsApi.credentials.js",
|
"dist/credentials/YourlsApi.credentials.js",
|
||||||
|
@ -402,6 +404,7 @@
|
||||||
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
"dist/nodes/ExecuteWorkflow/ExecuteWorkflow.node.js",
|
||||||
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
"dist/nodes/Facebook/FacebookGraphApi.node.js",
|
||||||
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
"dist/nodes/Facebook/FacebookTrigger.node.js",
|
||||||
|
"dist/nodes/Figma/FigmaTrigger.node.js",
|
||||||
"dist/nodes/FileMaker/FileMaker.node.js",
|
"dist/nodes/FileMaker/FileMaker.node.js",
|
||||||
"dist/nodes/Flow/Flow.node.js",
|
"dist/nodes/Flow/Flow.node.js",
|
||||||
"dist/nodes/Flow/FlowTrigger.node.js",
|
"dist/nodes/Flow/FlowTrigger.node.js",
|
||||||
|
@ -637,6 +640,7 @@
|
||||||
"dist/nodes/WooCommerce/WooCommerce.node.js",
|
"dist/nodes/WooCommerce/WooCommerce.node.js",
|
||||||
"dist/nodes/WooCommerce/WooCommerceTrigger.node.js",
|
"dist/nodes/WooCommerce/WooCommerceTrigger.node.js",
|
||||||
"dist/nodes/Wordpress/Wordpress.node.js",
|
"dist/nodes/Wordpress/Wordpress.node.js",
|
||||||
|
"dist/nodes/Workable/WorkableTrigger.node.js",
|
||||||
"dist/nodes/WorkflowTrigger/WorkflowTrigger.node.js",
|
"dist/nodes/WorkflowTrigger/WorkflowTrigger.node.js",
|
||||||
"dist/nodes/WriteBinaryFile/WriteBinaryFile.node.js",
|
"dist/nodes/WriteBinaryFile/WriteBinaryFile.node.js",
|
||||||
"dist/nodes/Wufoo/WufooTrigger.node.js",
|
"dist/nodes/Wufoo/WufooTrigger.node.js",
|
||||||
|
|
Loading…
Reference in a new issue