🔀 Merge branch 'i18n-v2'
1
.gitignore
vendored
|
@ -12,6 +12,5 @@ _START_PACKAGE
|
|||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
vetur.config.js
|
||||
nodelinter.config.json
|
||||
packages/*/package-lock.json
|
||||
|
|
18
package-lock.json
generated
|
@ -12523,6 +12523,14 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.178.tgz",
|
||||
"integrity": "sha512-0d5Wd09ItQWH1qFbEyQ7oTQ3GZrMfth5JkbN3EvTKLXcHLRDSXeLnlvlOn0wvxVIwK5o2M8JzP/OWz7T3NRsbw=="
|
||||
},
|
||||
"@types/lodash.camelcase": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.camelcase/-/lodash.camelcase-4.3.6.tgz",
|
||||
"integrity": "sha512-hd/TEuPd76Jtf1xEq85CHbCqR+iqvs5IOKyrYbiaOg69BRQgPN9XkvLj8Jl8rBp/dfJ2wQ1AVcP8mZmybq7kIg==",
|
||||
"requires": {
|
||||
"@types/lodash": "*"
|
||||
}
|
||||
},
|
||||
"@types/lodash.get": {
|
||||
"version": "4.4.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/lodash.get/-/lodash.get-4.4.6.tgz",
|
||||
|
@ -31861,6 +31869,11 @@
|
|||
"integrity": "sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
|
@ -42650,6 +42663,11 @@
|
|||
"resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
|
||||
"integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog=="
|
||||
},
|
||||
"vue-i18n": {
|
||||
"version": "8.26.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.26.7.tgz",
|
||||
"integrity": "sha512-7apa5PvRg1YCLoraE3lOgpCG8hJGupLCtywQWedWsgBbvF0TOgFvhitqK9xRH0PBGG1G8aiJz9oklyNDFfDxLg=="
|
||||
},
|
||||
"vue-inbrowser-compiler-utils": {
|
||||
"version": "4.43.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-inbrowser-compiler-utils/-/vue-inbrowser-compiler-utils-4.43.0.tgz",
|
||||
|
|
|
@ -689,6 +689,13 @@ const config = convict({
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
defaultLocale: {
|
||||
doc: 'Default locale for the UI',
|
||||
format: String,
|
||||
default: 'en',
|
||||
env: 'N8N_DEFAULT_LOCALE',
|
||||
},
|
||||
});
|
||||
|
||||
// Overwrite default configuration with settings which got defined in
|
||||
|
|
|
@ -403,6 +403,7 @@ export interface IN8nUISettings {
|
|||
instanceId: string;
|
||||
telemetry: ITelemetrySettings;
|
||||
personalizationSurvey: IPersonalizationSurvey;
|
||||
defaultLocale: string;
|
||||
}
|
||||
|
||||
export interface IPersonalizationSurveyAnswers {
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
import {
|
||||
INodeType,
|
||||
INodeTypeData,
|
||||
INodeTypeDescription,
|
||||
INodeTypes,
|
||||
INodeVersionedType,
|
||||
NodeHelpers,
|
||||
|
@ -18,7 +19,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
// polling nodes the polling times
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const nodeTypeData of Object.values(nodeTypes)) {
|
||||
const nodeType = NodeHelpers.getVersionedTypeNode(nodeTypeData.type);
|
||||
const nodeType = NodeHelpers.getVersionedNodeType(nodeTypeData.type);
|
||||
const applyParameters = NodeHelpers.getSpecialNodeParameters(nodeType);
|
||||
|
||||
if (applyParameters.length) {
|
||||
|
@ -39,11 +40,29 @@ class NodeTypesClass implements INodeTypes {
|
|||
return this.nodeTypes[nodeType].type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of `getByNameAndVersion` that includes the node's source path, used to locate a node's translations.
|
||||
*/
|
||||
getWithSourcePath(
|
||||
nodeTypeName: string,
|
||||
version: number,
|
||||
): { description: INodeTypeDescription } & { sourcePath: string } {
|
||||
const nodeType = this.nodeTypes[nodeTypeName];
|
||||
|
||||
if (!nodeType) {
|
||||
throw new Error(`Unknown node type: ${nodeTypeName}`);
|
||||
}
|
||||
|
||||
const { description } = NodeHelpers.getVersionedNodeType(nodeType.type, version);
|
||||
|
||||
return { description: { ...description }, sourcePath: nodeType.sourcePath };
|
||||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
if (this.nodeTypes[nodeType] === undefined) {
|
||||
throw new Error(`The node-type "${nodeType}" is not known!`);
|
||||
}
|
||||
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -24,8 +24,12 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
/* eslint-disable import/no-dynamic-require */
|
||||
/* eslint-disable no-await-in-loop */
|
||||
|
||||
import * as express from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { dirname as pathDirname, join as pathJoin, resolve as pathResolve } from 'path';
|
||||
import { FindManyOptions, getConnectionManager, In, IsNull, LessThanOrEqual, Not } from 'typeorm';
|
||||
import * as bodyParser from 'body-parser';
|
||||
|
@ -144,6 +148,7 @@ import { InternalHooksManager } from './InternalHooksManager';
|
|||
import { TagEntity } from './databases/entities/TagEntity';
|
||||
import { WorkflowEntity } from './databases/entities/WorkflowEntity';
|
||||
import { NameRequest } from './WorkflowHelpers';
|
||||
import { getNodeTranslationPath } from './TranslationHelpers';
|
||||
|
||||
require('body-parser-xml')(bodyParser);
|
||||
|
||||
|
@ -280,6 +285,7 @@ class App {
|
|||
personalizationSurvey: {
|
||||
shouldShow: false,
|
||||
},
|
||||
defaultLocale: config.get('defaultLocale'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -1151,13 +1157,13 @@ class App {
|
|||
|
||||
if (onlyLatest) {
|
||||
allNodes.forEach((nodeData) => {
|
||||
const nodeType = NodeHelpers.getVersionedTypeNode(nodeData);
|
||||
const nodeType = NodeHelpers.getVersionedNodeType(nodeData);
|
||||
const nodeInfo: INodeTypeDescription = getNodeDescription(nodeType);
|
||||
returnData.push(nodeInfo);
|
||||
});
|
||||
} else {
|
||||
allNodes.forEach((nodeData) => {
|
||||
const allNodeTypes = NodeHelpers.getVersionedTypeNodeAll(nodeData);
|
||||
const allNodeTypes = NodeHelpers.getVersionedNodeTypeAll(nodeData);
|
||||
allNodeTypes.forEach((element) => {
|
||||
const nodeInfo: INodeTypeDescription = getNodeDescription(element);
|
||||
returnData.push(nodeInfo);
|
||||
|
@ -1176,17 +1182,60 @@ class App {
|
|||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<INodeTypeDescription[]> => {
|
||||
const nodeInfos = _.get(req, 'body.nodeInfos', []) as INodeTypeNameVersion[];
|
||||
const nodeTypes = NodeTypes();
|
||||
|
||||
const returnData: INodeTypeDescription[] = [];
|
||||
nodeInfos.forEach((nodeInfo) => {
|
||||
const nodeType = nodeTypes.getByNameAndVersion(nodeInfo.name, nodeInfo.version);
|
||||
if (nodeType?.description) {
|
||||
returnData.push(nodeType.description);
|
||||
const { defaultLocale } = this.frontendSettings;
|
||||
|
||||
if (defaultLocale === 'en') {
|
||||
return nodeInfos.reduce<INodeTypeDescription[]>((acc, { name, version }) => {
|
||||
const { description } = NodeTypes().getByNameAndVersion(name, version);
|
||||
acc.push(description);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
async function populateTranslation(
|
||||
name: string,
|
||||
version: number,
|
||||
nodeTypes: INodeTypeDescription[],
|
||||
) {
|
||||
const { description, sourcePath } = NodeTypes().getWithSourcePath(name, version);
|
||||
const translationPath = await getNodeTranslationPath(sourcePath, defaultLocale);
|
||||
|
||||
try {
|
||||
const translation = await readFile(translationPath, 'utf8');
|
||||
description.translation = JSON.parse(translation);
|
||||
} catch (error) {
|
||||
// ignore - no translation at expected translation path
|
||||
}
|
||||
});
|
||||
|
||||
return returnData;
|
||||
nodeTypes.push(description);
|
||||
}
|
||||
|
||||
const nodeTypes: INodeTypeDescription[] = [];
|
||||
|
||||
const promises = nodeInfos.map(async ({ name, version }) =>
|
||||
populateTranslation(name, version, nodeTypes),
|
||||
);
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
return nodeTypes;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// Returns node information based on node names and versions
|
||||
this.app.get(
|
||||
`/${this.restEndpoint}/node-translation-headers`,
|
||||
ResponseHelper.send(
|
||||
async (req: express.Request, res: express.Response): Promise<object | void> => {
|
||||
const packagesPath = pathJoin(__dirname, '..', '..', '..');
|
||||
const headersPath = pathJoin(packagesPath, 'nodes-base', 'dist', 'nodes', 'headers');
|
||||
try {
|
||||
return require(headersPath);
|
||||
} catch (error) {
|
||||
res.status(500).send('Failed to find headers file');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
@ -1584,6 +1633,7 @@ class App {
|
|||
// No idea if multiple where parameters make db search
|
||||
// slower but to be sure that that is not the case we
|
||||
// remove all unnecessary fields in case the id is defined.
|
||||
// @ts-ignore
|
||||
findQuery.where = { id: findQuery.where.id };
|
||||
}
|
||||
}
|
||||
|
@ -2859,6 +2909,12 @@ export async function start(): Promise<void> {
|
|||
console.log(`n8n ready on ${ADDRESS}, port ${PORT}`);
|
||||
console.log(`Version: ${versions.cli}`);
|
||||
|
||||
const defaultLocale = config.get('defaultLocale');
|
||||
|
||||
if (defaultLocale !== 'en') {
|
||||
console.log(`Locale: ${defaultLocale}`);
|
||||
}
|
||||
|
||||
await app.externalHooks.run('n8n.ready', [app]);
|
||||
const cpus = os.cpus();
|
||||
const diagnosticInfo: IDiagnosticInfo = {
|
||||
|
|
39
packages/cli/src/TranslationHelpers.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { join, dirname } from 'path';
|
||||
import { readdir } from 'fs/promises';
|
||||
import { Dirent } from 'fs';
|
||||
|
||||
const ALLOWED_VERSIONED_DIRNAME_LENGTH = [2, 3]; // v1, v10
|
||||
|
||||
function isVersionedDirname(dirent: Dirent) {
|
||||
if (!dirent.isDirectory()) return false;
|
||||
|
||||
return (
|
||||
ALLOWED_VERSIONED_DIRNAME_LENGTH.includes(dirent.name.length) &&
|
||||
dirent.name.toLowerCase().startsWith('v')
|
||||
);
|
||||
}
|
||||
|
||||
async function getMaxVersion(from: string) {
|
||||
const entries = await readdir(from, { withFileTypes: true });
|
||||
|
||||
const dirnames = entries.reduce<string[]>((acc, cur) => {
|
||||
if (isVersionedDirname(cur)) acc.push(cur.name);
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
if (!dirnames.length) return null;
|
||||
|
||||
return Math.max(...dirnames.map((d) => parseInt(d.charAt(1), 10)));
|
||||
}
|
||||
|
||||
export async function getNodeTranslationPath(
|
||||
nodeSourcePath: string,
|
||||
language: string,
|
||||
): Promise<string> {
|
||||
const nodeDir = dirname(nodeSourcePath);
|
||||
const maxVersion = await getMaxVersion(nodeDir);
|
||||
|
||||
return maxVersion
|
||||
? join(nodeDir, `v${maxVersion}`, 'translations', `${language}.json`)
|
||||
: join(nodeDir, 'translations', `${language}.json`);
|
||||
}
|
|
@ -723,7 +723,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
async init(nodeTypes: INodeTypeData): Promise<void> {}
|
||||
|
||||
getAll(): INodeType[] {
|
||||
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedTypeNode(data.type));
|
||||
return Object.values(this.nodeTypes).map((data) => NodeHelpers.getVersionedNodeType(data.type));
|
||||
}
|
||||
|
||||
getByName(nodeType: string): INodeType {
|
||||
|
@ -731,7 +731,7 @@ class NodeTypesClass implements INodeTypes {
|
|||
}
|
||||
|
||||
getByNameAndVersion(nodeType: string, version?: number): INodeType {
|
||||
return NodeHelpers.getVersionedTypeNode(this.nodeTypes[nodeType].type, version);
|
||||
return NodeHelpers.getVersionedNodeType(this.nodeTypes[nodeType].type, version);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"n8n-design-system": "~0.9.0",
|
||||
"timeago.js": "^4.0.2",
|
||||
"v-click-outside": "^3.1.2",
|
||||
"vue-fragment": "^1.5.2"
|
||||
"vue-fragment": "^1.5.2",
|
||||
"vue-i18n": "^8.26.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.35",
|
||||
|
@ -39,6 +40,7 @@
|
|||
"@types/express": "^4.17.6",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jest": "^26.0.13",
|
||||
"@types/lodash.camelcase": "^4.3.6",
|
||||
"@types/lodash.get": "^4.4.6",
|
||||
"@types/lodash.set": "^4.3.6",
|
||||
"@types/node": "14.17.27",
|
||||
|
@ -68,15 +70,16 @@
|
|||
"jquery": "^3.4.1",
|
||||
"jshint": "^2.9.7",
|
||||
"jsplumb": "2.15.4",
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.debounce": "^4.0.8",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.set": "^4.3.2",
|
||||
"n8n-workflow": "~0.79.0",
|
||||
"sass": "^1.26.5",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"prismjs": "^1.17.1",
|
||||
"quill": "^2.0.0-dev.3",
|
||||
"quill-autoformat": "^0.1.1",
|
||||
"sass": "^1.26.5",
|
||||
"sass-loader": "^8.0.2",
|
||||
"string-template-parser": "^1.2.6",
|
||||
"ts-jest": "^26.3.0",
|
||||
|
|
|
@ -163,6 +163,7 @@ export interface IRestApi {
|
|||
getPastExecutions(filter: object, limit: number, lastId?: string | number, firstId?: string | number): Promise<IExecutionsListResponse>;
|
||||
stopCurrentExecution(executionId: string): Promise<IExecutionsStopData>;
|
||||
makeRestApiRequest(method: string, endpoint: string, data?: any): Promise<any>; // tslint:disable-line:no-any
|
||||
getNodeTranslationHeaders(): Promise<INodeTranslationHeaders>;
|
||||
getNodeTypes(onlyLatest?: boolean): Promise<INodeTypeDescription[]>;
|
||||
getNodesInformation(nodeInfos: INodeTypeNameVersion[]): Promise<INodeTypeDescription[]>;
|
||||
getNodeParameterOptions(nodeTypeAndVersion: INodeTypeNameVersion, path: string, methodName: string, currentNodeParameters: INodeParameters, credentials?: INodeCredentials): Promise<INodePropertyOptions[]>;
|
||||
|
@ -180,6 +181,15 @@ export interface IRestApi {
|
|||
getTimezones(): Promise<IDataObject>;
|
||||
}
|
||||
|
||||
export interface INodeTranslationHeaders {
|
||||
data: {
|
||||
[key: string]: {
|
||||
displayName: string;
|
||||
description: string;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBinaryDisplayData {
|
||||
index: number;
|
||||
key: string;
|
||||
|
@ -523,6 +533,7 @@ export interface IN8nUISettings {
|
|||
instanceId: string;
|
||||
personalizationSurvey?: IPersonalizationSurvey;
|
||||
telemetry: ITelemetrySettings;
|
||||
defaultLocale: string;
|
||||
}
|
||||
|
||||
export interface IWorkflowSettings extends IWorkflowSettingsWorkflow {
|
||||
|
@ -636,6 +647,8 @@ export interface IRootState {
|
|||
activeActions: string[];
|
||||
activeNode: string | null;
|
||||
baseUrl: string;
|
||||
credentialTextRenderKeys: { nodeType: string; credentialType: string; } | null;
|
||||
defaultLocale: string;
|
||||
endpointWebhook: string;
|
||||
endpointWebhookTest: string;
|
||||
executionId: string | null;
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<span>
|
||||
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" title="About n8n" :before-close="closeDialog">
|
||||
<el-dialog class="n8n-about" :visible="dialogVisible" append-to-body width="50%" :title="$locale.baseText('about.aboutN8n')" :before-close="closeDialog">
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
n8n Version:
|
||||
{{ $locale.baseText('about.n8nVersion') }}
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
{{versionCli}}
|
||||
{{ versionCli }}
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
Source Code:
|
||||
{{ $locale.baseText('about.sourceCode') }}
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<a href="https://github.com/n8n-io/n8n" target="_blank">https://github.com/n8n-io/n8n</a>
|
||||
|
@ -20,15 +20,17 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="8" class="info-name">
|
||||
License:
|
||||
{{ $locale.baseText('about.license') }}
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">Apache 2.0 with Commons Clause</a>
|
||||
<a href="https://github.com/n8n-io/n8n/blob/master/packages/cli/LICENSE.md" target="_blank">
|
||||
{{ $locale.baseText('about.apacheWithCommons20Clause') }}
|
||||
</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<div class="action-buttons">
|
||||
<n8n-button @click="closeDialog" label="Close" />
|
||||
<n8n-button @click="closeDialog" :label="$locale.baseText('about.close')" />
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
|
|
@ -4,18 +4,18 @@
|
|||
@click.stop="closeWindow"
|
||||
size="small"
|
||||
class="binary-data-window-back"
|
||||
title="Back to overview page"
|
||||
:title="$locale.baseText('binaryDataDisplay.backToOverviewPage')"
|
||||
icon="arrow-left"
|
||||
label="Back to list"
|
||||
:label="$locale.baseText('binaryDataDisplay.backToList')"
|
||||
/>
|
||||
|
||||
<div class="binary-data-window-wrapper">
|
||||
<div v-if="!binaryData">
|
||||
Data to display did not get found
|
||||
{{ $locale.baseText('binaryDataDisplay.noDataFoundToDisplay') }}
|
||||
</div>
|
||||
<video v-else-if="binaryData.mimeType && binaryData.mimeType.startsWith('video/')" controls autoplay>
|
||||
<source :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" :type="binaryData.mimeType">
|
||||
Your browser does not support the video element. Kindly update it to latest version.
|
||||
{{ $locale.baseText('binaryDataDisplay.yourBrowserDoesNotSupport') }}
|
||||
</video>
|
||||
<embed v-else :src="'data:' + binaryData.mimeType + ';base64,' + binaryData.data" class="binary-data" :class="embedClass"/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible">
|
||||
<el-dialog :visible="dialogVisible" append-to-body :close-on-click-modal="false" width="80%" :title="`Edit ${parameter.displayName}`" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" append-to-body :close-on-click-modal="false" width="80%" :title="`${$locale.baseText('codeEdit.edit')} ${$locale.nodeText().topParameterDisplayName(parameter)}`" :before-close="closeDialog">
|
||||
<div class="ignore-key-press">
|
||||
<n8n-input-label :label="parameter.displayName">
|
||||
<n8n-input-label :label="$locale.nodeText().topParameterDisplayName(parameter)">
|
||||
<div :class="$style.editor" @keydown.stop>
|
||||
<prism-editor :lineNumbers="true" :code="value" :readonly="isReadOnly" @change="valueChanged" language="js"></prism-editor>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<div @keydown.stop class="collection-parameter">
|
||||
<div class="collection-parameter-wrapper">
|
||||
<div v-if="getProperties.length === 0" class="no-items-exist">
|
||||
<n8n-text size="small">Currently no properties exist</n8n-text>
|
||||
<n8n-text size="small">{{ $locale.baseText('collectionParameter.noProperties') }}</n8n-text>
|
||||
</div>
|
||||
|
||||
<parameter-input-list :parameters="getProperties" :nodeValues="nodeValues" :path="path" :hideDelete="hideDelete" :indent="true" @valueChanged="valueChanged" />
|
||||
|
@ -19,7 +19,7 @@
|
|||
<n8n-option
|
||||
v-for="item in parameterOptions"
|
||||
:key="item.name"
|
||||
:label="item.displayName"
|
||||
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item)"
|
||||
:value="item.name">
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -67,7 +67,8 @@ export default mixins(
|
|||
},
|
||||
computed: {
|
||||
getPlaceholderText (): string {
|
||||
return this.parameter.placeholder ? this.parameter.placeholder : 'Choose Option To Add';
|
||||
const placeholder = this.$locale.nodeText().placeholder(this.parameter);
|
||||
return placeholder ? placeholder : this.$locale.baseText('collectionParameter.choose');
|
||||
},
|
||||
getProperties (): INodeProperties[] {
|
||||
const returnProperties = [];
|
||||
|
|
|
@ -38,7 +38,7 @@ export default mixins(copyPaste, showMessage).extend({
|
|||
this.copyToClipboard(this.$props.copyContent);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
title: this.$locale.baseText('credentialsEdit.showMessage.title'),
|
||||
message: this.$props.successMessage,
|
||||
type: 'success',
|
||||
});
|
||||
|
|
|
@ -3,17 +3,17 @@
|
|||
<banner
|
||||
v-show="showValidationWarning"
|
||||
theme="danger"
|
||||
message="Please check the errors below"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.pleaseCheckTheErrorsBelow')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-if="authError && !showValidationWarning"
|
||||
theme="danger"
|
||||
message="Couldn’t connect with these settings"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.couldntConnectWithTheseSettings')"
|
||||
:details="authError"
|
||||
buttonLabel="Retry"
|
||||
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
||||
buttonLoadingLabel="Retrying"
|
||||
buttonTitle="Retry credentials test"
|
||||
:buttonTitle="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
||||
:buttonLoading="isRetesting"
|
||||
@click="$emit('retest')"
|
||||
/>
|
||||
|
@ -21,35 +21,37 @@
|
|||
<banner
|
||||
v-show="showOAuthSuccessBanner && !showValidationWarning"
|
||||
theme="success"
|
||||
message="Account connected"
|
||||
buttonLabel="Reconnect"
|
||||
buttonTitle="Reconnect OAuth Credentials"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.accountConnected')"
|
||||
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.reconnect')"
|
||||
:buttonTitle="$locale.baseText('credentialEdit.credentialConfig.reconnectOAuth2Credential')"
|
||||
@click="$emit('oauth')"
|
||||
/>
|
||||
|
||||
<banner
|
||||
v-show="testedSuccessfully && !showValidationWarning"
|
||||
theme="success"
|
||||
message="Connection tested successfully"
|
||||
buttonLabel="Retry"
|
||||
buttonLoadingLabel="Retrying"
|
||||
buttonTitle="Retry credentials test"
|
||||
:message="$locale.baseText('credentialEdit.credentialConfig.connectionTestedSuccessfully')"
|
||||
:buttonLabel="$locale.baseText('credentialEdit.credentialConfig.retry')"
|
||||
:buttonLoadingLabel="$locale.baseText('credentialEdit.credentialConfig.retrying')"
|
||||
:buttonTitle="$locale.baseText('credentialEdit.credentialConfig.retryCredentialTest')"
|
||||
:buttonLoading="isRetesting"
|
||||
@click="$emit('retest')"
|
||||
/>
|
||||
|
||||
<n8n-info-tip v-if="documentationUrl && credentialProperties.length">
|
||||
Need help filling out these fields?
|
||||
<a :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open docs</a>
|
||||
{{ $locale.baseText('credentialEdit.credentialConfig.needHelpFillingOutTheseFields') }}
|
||||
<a :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">
|
||||
{{ $locale.baseText('credentialEdit.credentialConfig.openDocs') }}
|
||||
</a>
|
||||
</n8n-info-tip>
|
||||
|
||||
<CopyInput
|
||||
v-if="isOAuthType && credentialProperties.length"
|
||||
label="OAuth Redirect URL"
|
||||
:label="$locale.baseText('credentialEdit.credentialConfig.oAuthRedirectUrl')"
|
||||
:copyContent="oAuthCallbackUrl"
|
||||
copyButtonText="Click to copy"
|
||||
:subtitle="`In ${appName}, use the URL above when prompted to enter an OAuth callback or redirect URL`"
|
||||
successMessage="Redirect URL copied to clipboard"
|
||||
:copyButtonText="$locale.baseText('credentialEdit.credentialConfig.clickToCopy')"
|
||||
:subtitle="$locale.baseText('credentialEdit.credentialConfig.subtitle', { interpolate: { appName } })"
|
||||
:successMessage="$locale.baseText('credentialEdit.credentialConfig.redirectUrlCopiedToClipboard')"
|
||||
/>
|
||||
|
||||
<CredentialInputs
|
||||
|
@ -70,7 +72,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICredentialType } from 'n8n-workflow';
|
||||
import { ICredentialType, INodeTypeDescription } from 'n8n-workflow';
|
||||
import { getAppNameFromCredType } from '../helpers';
|
||||
|
||||
import Vue from 'vue';
|
||||
|
@ -78,8 +80,11 @@ import Banner from '../Banner.vue';
|
|||
import CopyInput from '../CopyInput.vue';
|
||||
import CredentialInputs from './CredentialInputs.vue';
|
||||
import OauthButton from './OauthButton.vue';
|
||||
import { restApi } from '@/components/mixins/restApi';
|
||||
import { addNodeTranslation } from '@/plugins/i18n';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default Vue.extend({
|
||||
export default mixins(restApi).extend({
|
||||
name: 'CredentialConfig',
|
||||
components: {
|
||||
Banner,
|
||||
|
@ -89,6 +94,7 @@ export default Vue.extend({
|
|||
},
|
||||
props: {
|
||||
credentialType: {
|
||||
type: Object,
|
||||
},
|
||||
credentialProperties: {
|
||||
type: Array,
|
||||
|
@ -121,6 +127,12 @@ export default Vue.extend({
|
|||
type: Boolean,
|
||||
},
|
||||
},
|
||||
async beforeMount() {
|
||||
if (this.$store.getters.defaultLocale !== 'en') {
|
||||
await this.findCredentialTextRenderKeys();
|
||||
await this.addNodeTranslationForCredential();
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
appName(): string {
|
||||
if (!this.credentialType) {
|
||||
|
@ -131,7 +143,7 @@ export default Vue.extend({
|
|||
(this.credentialType as ICredentialType).displayName,
|
||||
);
|
||||
|
||||
return appName || "the service you're connecting to";
|
||||
return appName || this.$locale.baseText('credentialEdit.credentialConfig.theServiceYouReConnectingTo');
|
||||
},
|
||||
credentialTypeName(): string {
|
||||
return (this.credentialType as ICredentialType).name;
|
||||
|
@ -165,6 +177,57 @@ export default Vue.extend({
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Find the keys needed by the mixin to render credential text, and place them in the Vuex store.
|
||||
*/
|
||||
async findCredentialTextRenderKeys() {
|
||||
const nodeTypes = await this.restApi().getNodeTypes();
|
||||
|
||||
// credential type name → node type name
|
||||
const map = nodeTypes.reduce<Record<string, string>>((acc, cur) => {
|
||||
if (!cur.credentials) return acc;
|
||||
|
||||
cur.credentials.forEach(cred => {
|
||||
if (acc[cred.name]) return;
|
||||
acc[cred.name] = cur.name;
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const renderKeys = {
|
||||
nodeType: map[this.credentialType.name],
|
||||
credentialType: this.credentialType.name,
|
||||
};
|
||||
|
||||
this.$store.commit('setCredentialTextRenderKeys', renderKeys);
|
||||
},
|
||||
|
||||
/**
|
||||
* Add to the translation object the node translation for the credential in the modal.
|
||||
*/
|
||||
async addNodeTranslationForCredential() {
|
||||
const { nodeType }: { nodeType: string } = this.$store.getters.credentialTextRenderKeys;
|
||||
const version = await this.getCurrentNodeVersion(nodeType);
|
||||
const nodeToBeFetched = [{ name: nodeType, version }];
|
||||
const nodesInfo = await this.restApi().getNodesInformation(nodeToBeFetched);
|
||||
const nodeInfo = nodesInfo.pop();
|
||||
|
||||
if (nodeInfo && nodeInfo.translation) {
|
||||
addNodeTranslation(nodeInfo.translation, this.$store.getters.defaultLocale);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current version for a node type.
|
||||
*/
|
||||
async getCurrentNodeVersion(targetNodeType: string) {
|
||||
const { allNodeTypes }: { allNodeTypes: INodeTypeDescription[] } = this.$store.getters;
|
||||
const found = allNodeTypes.find(nodeType => nodeType.name === targetNodeType);
|
||||
|
||||
return found ? found.version : 1;
|
||||
},
|
||||
|
||||
onDataChange (event: { name: string; value: string | number | boolean | Date | null }): void {
|
||||
this.$emit('change', event);
|
||||
},
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
<n8n-icon-button
|
||||
v-if="currentCredential"
|
||||
size="small"
|
||||
title="Delete"
|
||||
:title="$locale.baseText('credentialEdit.credentialEdit.delete')"
|
||||
icon="trash"
|
||||
type="text"
|
||||
:disabled="isSaving"
|
||||
|
@ -36,7 +36,9 @@
|
|||
v-if="hasUnsavedChanges || credentialId"
|
||||
:saved="!hasUnsavedChanges && !isTesting"
|
||||
:isSaving="isSaving || isTesting"
|
||||
:savingLabel="isTesting ? 'Testing' : 'Saving'"
|
||||
:savingLabel="isTesting
|
||||
? $locale.baseText('credentialEdit.credentialEdit.testing')
|
||||
: $locale.baseText('credentialEdit.credentialEdit.saving')"
|
||||
@click="saveCredential"
|
||||
/>
|
||||
</div>
|
||||
|
@ -53,10 +55,10 @@
|
|||
:light="true"
|
||||
>
|
||||
<n8n-menu-item index="connection" :class="$style.credTab"
|
||||
><span slot="title">Connection</span></n8n-menu-item
|
||||
><span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.connection') }}</span></n8n-menu-item
|
||||
>
|
||||
<n8n-menu-item index="details" :class="$style.credTab"
|
||||
><span slot="title">Details</span></n8n-menu-item
|
||||
><span slot="title">{{ $locale.baseText('credentialEdit.credentialEdit.details') }}</span></n8n-menu-item
|
||||
>
|
||||
</n8n-menu>
|
||||
</div>
|
||||
|
@ -349,20 +351,20 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
if (this.hasUnsavedChanges) {
|
||||
const displayName = this.credentialType ? this.credentialType.displayName : '';
|
||||
keepEditing = await this.confirmMessage(
|
||||
`Are you sure you want to throw away the changes you made to the ${displayName} credential?`,
|
||||
'Close without saving?',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.message', { interpolate: { credentialDisplayName: displayName } }),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.headline'),
|
||||
null,
|
||||
'Keep editing',
|
||||
'Close',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.cancelButtonText'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose1.confirmButtonText'),
|
||||
);
|
||||
}
|
||||
else if (this.isOAuthType && !this.isOAuthConnected) {
|
||||
keepEditing = await this.confirmMessage(
|
||||
`You need to connect your credential for it to work`,
|
||||
'Close without connecting?',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.message'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.headline'),
|
||||
null,
|
||||
'Keep editing',
|
||||
'Close',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.cancelButtonText'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.beforeClose2.confirmButtonText'),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -400,7 +402,9 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.$store.getters['credentials/getCredentialTypeByName'](name);
|
||||
|
||||
if (!credentialsData) {
|
||||
throw new Error(`Could not find credentials of type: ${name}`);
|
||||
throw new Error(
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.couldNotFindCredentialOfType') + ':' + name,
|
||||
);
|
||||
}
|
||||
|
||||
if (credentialsData.extends === undefined) {
|
||||
|
@ -436,7 +440,7 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
});
|
||||
if (!currentCredentials) {
|
||||
throw new Error(
|
||||
`Could not find the credentials with the id: ${this.credentialId}`,
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.couldNotFindCredentialWithId') + ':' + this.credentialId,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -448,11 +452,11 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.nodeAccess[access.nodeType] = access;
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
} catch (error) {
|
||||
this.$showError(
|
||||
e,
|
||||
'Problem loading credentials',
|
||||
'There was a problem loading the credentials:',
|
||||
error,
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.loadCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.loadCredential.message'),
|
||||
);
|
||||
this.closeDialog();
|
||||
|
||||
|
@ -657,8 +661,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem creating credentials',
|
||||
'There was a problem creating the credentials:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.createCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.createCredential.message'),
|
||||
);
|
||||
|
||||
return null;
|
||||
|
@ -686,8 +690,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem updating credentials',
|
||||
'There was a problem updating the credentials:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.updateCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.updateCredential.message'),
|
||||
);
|
||||
|
||||
return null;
|
||||
|
@ -708,10 +712,10 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
const savedCredentialName = this.currentCredential.name;
|
||||
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
`Are you sure you want to delete "${savedCredentialName}" credentials?`,
|
||||
'Delete Credentials?',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.message', { interpolate: { savedCredentialName } }),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.headline'),
|
||||
null,
|
||||
'Yes, delete!',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.confirmMessage.deleteCredential.confirmButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
|
@ -727,8 +731,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'Problem deleting credentials',
|
||||
'There was a problem deleting the credentials:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.deleteCredential.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.deleteCredential.message'),
|
||||
);
|
||||
this.isDeleting = false;
|
||||
|
||||
|
@ -740,8 +744,11 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials deleted',
|
||||
message: `The credential "${savedCredentialName}" was deleted!`,
|
||||
title: this.$locale.baseText('credentialEdit.credentialEdit.showMessage.title'),
|
||||
message: this.$locale.baseText(
|
||||
'credentialEdit.credentialEdit.showMessage.message',
|
||||
{ interpolate: { savedCredentialName } },
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
this.closeDialog();
|
||||
|
@ -778,8 +785,8 @@ export default mixins(showMessage, nodeHelpers).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
'OAuth Authorization Error',
|
||||
'Error generating authorization URL:',
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.title'),
|
||||
this.$locale.baseText('credentialEdit.credentialEdit.showError.generateAuthorizationUrl.message'),
|
||||
);
|
||||
|
||||
return;
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div :class="$style.container">
|
||||
<el-row>
|
||||
<el-col :span="8" :class="$style.accessLabel">
|
||||
<n8n-text :compact="true" :bold="true">Allow use by</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.allowUseBy') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div
|
||||
|
@ -11,7 +13,10 @@
|
|||
:class="$style.valueLabel"
|
||||
>
|
||||
<el-checkbox
|
||||
:label="node.displayName"
|
||||
:label="$locale.headerText({
|
||||
key: `headers.${shortNodeType(node)}.displayName`,
|
||||
fallback: node.displayName,
|
||||
})"
|
||||
:value="!!nodeAccess[node.name]"
|
||||
@change="(val) => onNodeAccessChange(node.name, val)"
|
||||
/>
|
||||
|
@ -20,7 +25,9 @@
|
|||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<n8n-text :compact="true" :bold="true">Created</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.created') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<n8n-text :compact="true"><TimeAgo :date="currentCredential.createdAt" :capitalize="true" /></n8n-text>
|
||||
|
@ -28,7 +35,9 @@
|
|||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<n8n-text :compact="true" :bold="true">Last modified</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.lastModified') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<n8n-text :compact="true"><TimeAgo :date="currentCredential.updatedAt" :capitalize="true" /></n8n-text>
|
||||
|
@ -36,10 +45,12 @@
|
|||
</el-row>
|
||||
<el-row v-if="currentCredential">
|
||||
<el-col :span="8" :class="$style.label">
|
||||
<n8n-text :compact="true" :bold="true">ID</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $locale.baseText('credentialEdit.credentialInfo.id') }}
|
||||
</n8n-text>
|
||||
</el-col>
|
||||
<el-col :span="16" :class="$style.valueLabel">
|
||||
<n8n-text :compact="true">{{currentCredential.id}}</n8n-text>
|
||||
<n8n-text :compact="true">{{ currentCredential.id }}</n8n-text>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
@ -49,6 +60,7 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import TimeAgo from '../TimeAgo.vue';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialInfo',
|
||||
|
@ -63,6 +75,9 @@ export default Vue.extend({
|
|||
value,
|
||||
});
|
||||
},
|
||||
shortNodeType(nodeType: INodeTypeDescription) {
|
||||
return this.$locale.shortNodeType(nodeType.name);
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -4,12 +4,12 @@
|
|||
v-if="isGoogleOAuthType"
|
||||
:src="basePath + 'google-signin-light.png'"
|
||||
:class="$style.googleIcon"
|
||||
alt="Sign in with Google"
|
||||
:alt="$locale.baseText('credentialEdit.oAuthButton.signInWithGoogle')"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
<n8n-button
|
||||
v-else
|
||||
label="Connect my account"
|
||||
:label="$locale.baseText('credentialEdit.oAuthButton.connectMyAccount')"
|
||||
size="large"
|
||||
@click.stop="$emit('click')"
|
||||
/>
|
||||
|
@ -18,6 +18,7 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
|
|
|
@ -2,32 +2,32 @@
|
|||
<Modal
|
||||
:name="CREDENTIAL_LIST_MODAL_KEY"
|
||||
width="80%"
|
||||
title="Credentials"
|
||||
:title="$locale.baseText('credentialsList.credentials')"
|
||||
>
|
||||
<template v-slot:content>
|
||||
<n8n-heading tag="h3" size="small" color="text-light">Your saved credentials:</n8n-heading>
|
||||
<n8n-heading tag="h3" size="small" color="text-light">{{ $locale.baseText('credentialsList.yourSavedCredentials') + ':' }}</n8n-heading>
|
||||
<div class="new-credentials-button">
|
||||
<n8n-button
|
||||
title="Create New Credentials"
|
||||
:title="$locale.baseText('credentialsList.createNewCredential')"
|
||||
icon="plus"
|
||||
label="Add New"
|
||||
:label="$locale.baseText('credentialsList.addNew')"
|
||||
size="large"
|
||||
@click="createCredential()"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-table :data="credentialsToDisplay" :default-sort = "{prop: 'name', order: 'ascending'}" stripe max-height="450" @row-click="editCredential">
|
||||
<el-table-column property="name" label="Name" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="type" label="Type" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" label="Created" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" label="Updated" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="name" :label="$locale.baseText('credentialsList.name')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="type" :label="$locale.baseText('credentialsList.type')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="createdAt" :label="$locale.baseText('credentialsList.created')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" :label="$locale.baseText('credentialsList.updated')" class-name="clickable" sortable></el-table-column>
|
||||
<el-table-column
|
||||
label="Operations"
|
||||
:label="$locale.baseText('credentialsList.operations')"
|
||||
width="120">
|
||||
<template slot-scope="scope">
|
||||
<div class="cred-operations">
|
||||
<n8n-icon-button title="Edit Credentials" @click.stop="editCredential(scope.row)" size="small" icon="pen" />
|
||||
<n8n-icon-button title="Delete Credentials" @click.stop="deleteCredential(scope.row)" size="small" icon="trash" />
|
||||
<n8n-icon-button :title="$locale.baseText('credentialsList.editCredential')" @click.stop="editCredential(scope.row)" size="small" icon="pen" />
|
||||
<n8n-icon-button :title="$locale.baseText('credentialsList.deleteCredential')" @click.stop="deleteCredential(scope.row)" size="small" icon="trash" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
@ -103,7 +103,16 @@ export default mixins(
|
|||
},
|
||||
|
||||
async deleteCredential (credential: ICredentialsResponse) {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure you want to delete "${credential.name}" credentials?`, 'Delete Credentials?', null, 'Yes, delete!');
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'credentialsList.confirmMessage.message',
|
||||
{ interpolate: { credentialName: credential.name }},
|
||||
),
|
||||
this.$locale.baseText('credentialsList.confirmMessage.headline'),
|
||||
null,
|
||||
this.$locale.baseText('credentialsList.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('credentialsList.confirmMessage.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
|
@ -112,7 +121,11 @@ export default mixins(
|
|||
try {
|
||||
await this.$store.dispatch('credentials/deleteCredential', {id: credential.id});
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem deleting credentials', 'There was a problem deleting the credentials:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('credentialsList.showError.deleteCredential.title'),
|
||||
this.$locale.baseText('credentialsList.showError.deleteCredential.message'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -121,8 +134,11 @@ export default mixins(
|
|||
this.updateNodesCredentialsIssues();
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Credentials deleted',
|
||||
message: `The credential "${credential.name}" was deleted!`,
|
||||
title: this.$locale.baseText('credentialsList.showMessage.title'),
|
||||
message: this.$locale.baseText(
|
||||
'credentialsList.showMessage.message',
|
||||
{ interpolate: { credentialName: credential.name }},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
|
|
@ -7,15 +7,15 @@
|
|||
maxWidth="460px"
|
||||
>
|
||||
<template slot="header">
|
||||
<h2 :class="$style.title">Add new credential</h2>
|
||||
<h2 :class="$style.title">{{ $locale.baseText('credentialSelectModal.addNewCredential') }}</h2>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<div>
|
||||
<div :class="$style.subtitle">Select an app or service to connect to</div>
|
||||
<div :class="$style.subtitle">{{ $locale.baseText('credentialSelectModal.selectAnAppOrServiceToConnectTo') }}</div>
|
||||
<n8n-select
|
||||
filterable
|
||||
defaultFirstOption
|
||||
placeholder="Search for app..."
|
||||
:placeholder="$locale.baseText('credentialSelectModal.searchForApp')"
|
||||
size="xlarge"
|
||||
ref="select"
|
||||
:value="selected"
|
||||
|
@ -35,7 +35,7 @@
|
|||
<template slot="footer">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button
|
||||
label="Continue"
|
||||
:label="$locale.baseText('credentialSelectModal.continue')"
|
||||
float="right"
|
||||
size="large"
|
||||
:disabled="!selected"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<transition name="fade">
|
||||
<div v-if="nodeType && showDocumentHelp" class="doc-help-wrapper">
|
||||
<svg id="help-logo" :href="documentationUrl" target="_blank" width="18px" height="18px" viewBox="0 0 18 18" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Node Documentation</title>
|
||||
<title>{{ $locale.baseText('dataDisplay.nodeDocumentation') }}</title>
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-1127.000000, -836.000000)" fill-rule="nonzero">
|
||||
<g transform="translate(1117.000000, 825.000000)">
|
||||
|
@ -31,7 +31,7 @@
|
|||
</svg>
|
||||
|
||||
<div class="text">
|
||||
Need help? <a id="doc-hyperlink" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open {{nodeType.displayName}} documentation</a>
|
||||
{{ $locale.baseText('dataDisplay.needHelp') }} <a id="doc-hyperlink" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">{{ $locale.baseText('dataDisplay.openDocumentationFor', { interpolate: { nodeTypeDisplayName: nodeType.displayName } }) }}</a>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template >
|
||||
<template>
|
||||
<span class="static-text-wrapper">
|
||||
<span v-show="!editActive" title="Click to change">
|
||||
<span v-show="!editActive" :title="$locale.baseText('displayWithChange.clickToChange')">
|
||||
<span class="static-text" @mousedown="startEdit">{{currentValue}}</span>
|
||||
</span>
|
||||
<span v-show="editActive">
|
||||
<input class="edit-field" ref="inputField" type="text" v-model="newValue" @keydown.enter.stop.prevent="setValue" @keydown.escape.stop.prevent="cancelEdit" @keydown.stop="noOp" @blur="cancelEdit" />
|
||||
<font-awesome-icon icon="times" @mousedown="cancelEdit" class="icons clickable" title="Cancel Edit" />
|
||||
<font-awesome-icon icon="check" @mousedown="setValue" class="icons clickable" title="Set Value" />
|
||||
<font-awesome-icon icon="times" @mousedown="cancelEdit" class="icons clickable" :title="$locale.baseText('displayWithChange.cancelEdit')" />
|
||||
<font-awesome-icon icon="check" @mousedown="setValue" class="icons clickable" :title="$locale.baseText('displayWithChange.setValue')" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
@ -33,6 +33,15 @@ export default mixins(genericHelpers).extend({
|
|||
return path.split('.').reduce((acc, part) => acc && acc[part], obj);
|
||||
};
|
||||
|
||||
if (this.keyName === 'name' && this.node.type.startsWith('n8n-nodes-base.')) {
|
||||
const shortNodeType = this.$locale.shortNodeType(this.node.type);
|
||||
|
||||
return this.$locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: getDescendantProp(this.node, this.keyName),
|
||||
});
|
||||
}
|
||||
|
||||
return getDescendantProp(this.node, this.keyName);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:name="modalName"
|
||||
:eventBus="modalBus"
|
||||
@enter="save"
|
||||
title="Duplicate Workflow"
|
||||
:title="$locale.baseText('duplicateWorkflowDialog.duplicateWorkflow')"
|
||||
:center="true"
|
||||
minWidth="420px"
|
||||
maxWidth="420px"
|
||||
|
@ -13,7 +13,7 @@
|
|||
<n8n-input
|
||||
v-model="name"
|
||||
ref="nameInput"
|
||||
placeholder="Enter workflow name"
|
||||
:placeholder="$locale.baseText('duplicateWorkflowDialog.enterWorkflowName')"
|
||||
:maxlength="MAX_WORKFLOW_NAME_LENGTH"
|
||||
/>
|
||||
<TagsDropdown
|
||||
|
@ -23,15 +23,15 @@
|
|||
@blur="onTagsBlur"
|
||||
@esc="onTagsEsc"
|
||||
@update="onTagsUpdate"
|
||||
placeholder="Choose or create a tag"
|
||||
:placeholder="$locale.baseText('duplicateWorkflowDialog.chooseOrCreateATag')"
|
||||
ref="dropdown"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot:footer="{ close }">
|
||||
<div :class="$style.footer">
|
||||
<n8n-button @click="save" :loading="isSaving" label="Save" float="right" />
|
||||
<n8n-button type="outline" @click="close" :disabled="isSaving" label="Cancel" float="right" />
|
||||
<n8n-button @click="save" :loading="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.save')" float="right" />
|
||||
<n8n-button type="outline" @click="close" :disabled="isSaving" :label="$locale.baseText('duplicateWorkflowDialog.cancel')" float="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@ -101,8 +101,8 @@ export default mixins(showMessage, workflowHelpers).extend({
|
|||
const name = this.name.trim();
|
||||
if (!name) {
|
||||
this.$showMessage({
|
||||
title: "Name missing",
|
||||
message: `Please enter a name.`,
|
||||
title: this.$locale.baseText('duplicateWorkflowDialog.showMessage.title'),
|
||||
message: this.$locale.baseText('duplicateWorkflowDialog.showMessage.message'),
|
||||
type: "error",
|
||||
});
|
||||
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="error-header">
|
||||
<div class="error-message">ERROR: {{error.message}}</div>
|
||||
<div class="error-message">{{ $locale.baseText('nodeErrorView.error') + ':' + error.message }}</div>
|
||||
<div class="error-description" v-if="error.description">{{error.description}}</div>
|
||||
</div>
|
||||
<details>
|
||||
<summary class="error-details__summary">
|
||||
<font-awesome-icon class="error-details__icon" icon="angle-right" /> Details
|
||||
<font-awesome-icon class="error-details__icon" icon="angle-right" /> {{ $locale.baseText('nodeErrorView.details') }}
|
||||
</summary>
|
||||
<div class="error-details__content">
|
||||
<div v-if="error.timestamp">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>Time</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.time') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{new Date(error.timestamp).toLocaleString()}}
|
||||
|
@ -22,7 +22,7 @@
|
|||
<div v-if="error.httpCode">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>HTTP-Code</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.httpCode') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
{{error.httpCode}}
|
||||
|
@ -32,13 +32,13 @@
|
|||
<div v-if="error.cause">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>Cause</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.cause') }}</span>
|
||||
<br>
|
||||
<span class="box-card__subtitle">Data below may contain sensitive information. Proceed with caution when sharing.</span>
|
||||
<span class="box-card__subtitle">{{ $locale.baseText('nodeErrorView.dataBelowMayContain') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="copy-button" v-if="displayCause">
|
||||
<n8n-icon-button @click="copyCause" title="Copy to Clipboard" icon="copy" />
|
||||
<n8n-icon-button @click="copyCause" :title="$locale.baseText('nodeErrorView.copyToClipboard')" icon="copy" />
|
||||
</div>
|
||||
<vue-json-pretty
|
||||
v-if="displayCause"
|
||||
|
@ -50,7 +50,7 @@
|
|||
class="json-data"
|
||||
/>
|
||||
<span v-else>
|
||||
<font-awesome-icon icon="info-circle" /> The error cause is too large to be displayed.
|
||||
<font-awesome-icon icon="info-circle" />{{ $locale.baseText('nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed') }}
|
||||
</span>
|
||||
</div>
|
||||
</el-card>
|
||||
|
@ -58,7 +58,7 @@
|
|||
<div v-if="error.stack">
|
||||
<el-card class="box-card" shadow="never">
|
||||
<div slot="header" class="clearfix box-card__title">
|
||||
<span>Stack</span>
|
||||
<span>{{ $locale.baseText('nodeErrorView.stack') }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<pre><code>{{error.stack}}</code></pre>
|
||||
|
@ -103,8 +103,8 @@ export default mixins(
|
|||
},
|
||||
copySuccess() {
|
||||
this.$showMessage({
|
||||
title: 'Copied to clipboard',
|
||||
message: '',
|
||||
title: this.$locale.baseText('nodeErrorView.showMessage.title'),
|
||||
message: this.$locale.baseText('nodeErrorView.showMessage.message'),
|
||||
type: 'info',
|
||||
});
|
||||
},
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<span>
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Workflow Executions ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`${$locale.baseText('executionsList.workflowExecutions')} ${combinedExecutions.length}/${finishedExecutionsCountEstimated === true ? '~' : ''}${combinedExecutionsCount}`" :before-close="closeDialog">
|
||||
<div class="filters">
|
||||
<el-row>
|
||||
<el-col :span="2" class="filter-headline">
|
||||
Filters:
|
||||
{{ $locale.baseText('executionsList.filters') }}:
|
||||
</el-col>
|
||||
<el-col :span="7">
|
||||
<n8n-select v-model="filter.workflowId" placeholder="Select Workflow" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-select v-model="filter.workflowId" :placeholder="$locale.baseText('executionsList.selectWorkflow')" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-option
|
||||
v-for="item in workflows"
|
||||
:key="item.id"
|
||||
|
@ -17,7 +17,7 @@
|
|||
</n8n-select>
|
||||
</el-col>
|
||||
<el-col :span="5" :offset="1">
|
||||
<n8n-select v-model="filter.status" placeholder="Select Status" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-select v-model="filter.status" :placeholder="$locale.baseText('executionsList.selectStatus')" size="medium" filterable @change="handleFilterChanged">
|
||||
<n8n-option
|
||||
v-for="item in statuses"
|
||||
:key="item.id"
|
||||
|
@ -27,15 +27,15 @@
|
|||
</n8n-select>
|
||||
</el-col>
|
||||
<el-col :span="4" :offset="5" class="autorefresh">
|
||||
<el-checkbox v-model="autoRefresh" @change="handleAutoRefreshToggle">Auto refresh</el-checkbox>
|
||||
<el-checkbox v-model="autoRefresh" @change="handleAutoRefreshToggle">{{ $locale.baseText('executionsList.autoRefresh') }}</el-checkbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<div class="selection-options">
|
||||
<span v-if="checkAll === true || isIndeterminate === true">
|
||||
Selected: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}}
|
||||
<n8n-icon-button title="Delete Selected" icon="trash" size="mini" @click="handleDeleteSelected" />
|
||||
{{ $locale.baseText('executionsList.selected') }}: {{numSelected}} / <span v-if="finishedExecutionsCountEstimated === true">~</span>{{finishedExecutionsCount}}
|
||||
<n8n-icon-button :title="$locale.baseText('executionsList.deleteSelected')" icon="trash" size="mini" @click="handleDeleteSelected" />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
@ -49,49 +49,45 @@
|
|||
<el-checkbox v-if="scope.row.stoppedAt !== undefined && scope.row.id" :value="selectedItems[scope.row.id.toString()] || checkAll" @change="handleCheckboxChanged(scope.row.id)" label=" "></el-checkbox>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="startedAt" label="Started At / ID" width="205">
|
||||
<el-table-column property="startedAt" :label="$locale.baseText('executionsList.startedAtId')" width="205">
|
||||
<template slot-scope="scope">
|
||||
{{convertToDisplayDate(scope.row.startedAt)}}<br />
|
||||
<small v-if="scope.row.id">ID: {{scope.row.id}}</small>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="workflowName" label="Name">
|
||||
<el-table-column property="workflowName" :label="$locale.baseText('executionsList.name')">
|
||||
<template slot-scope="scope">
|
||||
<span class="workflow-name">
|
||||
{{scope.row.workflowName || '[UNSAVED WORKFLOW]'}}
|
||||
{{ scope.row.workflowName || $locale.baseText('executionsList.unsavedWorkflow') }}
|
||||
</span>
|
||||
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
(running)
|
||||
({{ $locale.baseText('executionsList.running') }})
|
||||
</span>
|
||||
<span v-if="scope.row.retryOf !== undefined">
|
||||
<br /><small>Retry of "{{scope.row.retryOf}}"</small>
|
||||
<br /><small>{{ $locale.baseText('executionsList.retryOf') }} "{{scope.row.retryOf}}"</small>
|
||||
</span>
|
||||
<span v-else-if="scope.row.retrySuccessId !== undefined">
|
||||
<br /><small>Success retry "{{scope.row.retrySuccessId}}"</small>
|
||||
<br /><small>{{ $locale.baseText('executionsList.successRetry') }} "{{scope.row.retrySuccessId}}"</small>
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Status" width="122" align="center">
|
||||
<el-table-column :label="$locale.baseText('executionsList.status')" width="122" align="center">
|
||||
<template slot-scope="scope" align="center">
|
||||
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="statusTooltipText(scope.row)"></div>
|
||||
|
||||
<span class="status-badge running" v-if="scope.row.waitTill">
|
||||
Waiting
|
||||
</span>
|
||||
<span class="status-badge running" v-else-if="scope.row.stoppedAt === undefined">
|
||||
Running
|
||||
<span class="status-badge running" v-if="scope.row.stoppedAt === undefined">
|
||||
{{ $locale.baseText('executionsList.running') }}
|
||||
</span>
|
||||
<span class="status-badge success" v-else-if="scope.row.finished">
|
||||
Success
|
||||
{{ $locale.baseText('executionsList.success') }}
|
||||
</span>
|
||||
<span class="status-badge error" v-else-if="scope.row.stoppedAt !== null">
|
||||
Error
|
||||
{{ $locale.baseText('executionsList.error') }}
|
||||
</span>
|
||||
<span class="status-badge warning" v-else>
|
||||
Unknown
|
||||
{{ $locale.baseText('executionsList.unknown') }}
|
||||
</span>
|
||||
</n8n-tooltip>
|
||||
|
||||
|
@ -102,20 +98,28 @@
|
|||
type="light"
|
||||
:theme="scope.row.stoppedAt === null ? 'warning': 'danger'"
|
||||
size="mini"
|
||||
title="Retry execution"
|
||||
:title="$locale.baseText('executionsList.retryExecution')"
|
||||
icon="redo"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'currentlySaved', row: scope.row}">Retry with currently saved workflow</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'original', row: scope.row}">Retry with original workflow</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'currentlySaved', row: scope.row}">
|
||||
{{ $locale.baseText('executionsList.retryWithCurrentlySavedWorkflow') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'original', row: scope.row}">
|
||||
{{ $locale.baseText('executionsList.retryWithOriginalworkflow') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="mode" label="Mode" width="100" align="center"></el-table-column>
|
||||
<el-table-column label="Running Time" width="150" align="center">
|
||||
<el-table-column property="mode" :label="$locale.baseText('executionsList.mode')" width="100" align="center">
|
||||
<template slot-scope="scope">
|
||||
{{ $locale.baseText(`executionsList.modes.${scope.row.mode}`) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$locale.baseText('executionsList.runningTime')" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
<font-awesome-icon icon="spinner" spin />
|
||||
|
@ -134,10 +138,10 @@
|
|||
<template slot-scope="scope">
|
||||
<div class="actions-container">
|
||||
<span v-if="scope.row.stoppedAt === undefined || scope.row.waitTill">
|
||||
<n8n-icon-button icon="stop" size="small" title="Stop Execution" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" />
|
||||
<n8n-icon-button icon="stop" size="small" :title="$locale.baseText('executionsList.stopExecution')" @click.stop="stopExecution(scope.row.id)" :loading="stoppingExecutions.includes(scope.row.id)" />
|
||||
</span>
|
||||
<span v-if="scope.row.stoppedAt !== undefined && scope.row.id" >
|
||||
<n8n-icon-button icon="folder-open" size="small" title="Open Past Execution" @click.stop="(e) => displayExecution(scope.row, e)" />
|
||||
<n8n-icon-button icon="folder-open" size="small" :title="$locale.baseText('executionsList.openPastExecution')" @click.stop="(e) => displayExecution(scope.row, e)" />
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -145,7 +149,7 @@
|
|||
</el-table>
|
||||
|
||||
<div class="load-more" v-if="finishedExecutionsCount > finishedExecutions.length || finishedExecutionsCountEstimated === true">
|
||||
<n8n-button icon="sync" title="Load More" label="Load More" @click="loadMore()" :loading="isDataLoading" />
|
||||
<n8n-button icon="sync" :title="$locale.baseText('executionsList.loadMore')" :label="$locale.baseText('executionsList.loadMore')" @click="loadMore()" :loading="isDataLoading" />
|
||||
</div>
|
||||
|
||||
</el-dialog>
|
||||
|
@ -224,32 +228,33 @@ export default mixins(
|
|||
|
||||
stoppingExecutions: [] as string[],
|
||||
workflows: [] as IWorkflowShortResponse[],
|
||||
statuses: [
|
||||
{
|
||||
id: 'ALL',
|
||||
name: 'Any Status',
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
name: 'Error',
|
||||
},
|
||||
{
|
||||
id: 'running',
|
||||
name: 'Running',
|
||||
},
|
||||
{
|
||||
id: 'success',
|
||||
name: 'Success',
|
||||
},
|
||||
{
|
||||
id: 'waiting',
|
||||
name: 'Waiting',
|
||||
},
|
||||
],
|
||||
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
statuses () {
|
||||
return [
|
||||
{
|
||||
id: 'ALL',
|
||||
name: this.$locale.baseText('executionsList.anyStatus'),
|
||||
},
|
||||
{
|
||||
id: 'error',
|
||||
name: this.$locale.baseText('executionsList.error'),
|
||||
},
|
||||
{
|
||||
id: 'running',
|
||||
name: this.$locale.baseText('executionsList.running'),
|
||||
},
|
||||
{
|
||||
id: 'success',
|
||||
name: this.$locale.baseText('executionsList.success'),
|
||||
},
|
||||
{
|
||||
id: 'waiting',
|
||||
name: this.$locale.baseText('executionsList.waiting'),
|
||||
},
|
||||
];
|
||||
},
|
||||
activeExecutions (): IExecutionsCurrentSummaryExtended[] {
|
||||
return this.$store.getters.getActiveExecutions;
|
||||
},
|
||||
|
@ -363,7 +368,16 @@ export default mixins(
|
|||
}
|
||||
},
|
||||
async handleDeleteSelected () {
|
||||
const deleteExecutions = await this.confirmMessage(`Are you sure that you want to delete the ${this.numSelected} selected executions?`, 'Delete Executions?', 'warning', 'Yes, delete!');
|
||||
const deleteExecutions = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'executionsList.confirmMessage.message',
|
||||
{ interpolate: { numSelected: this.numSelected.toString() }},
|
||||
),
|
||||
this.$locale.baseText('executionsList.confirmMessage.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('executionsList.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('executionsList.confirmMessage.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteExecutions === false) {
|
||||
return;
|
||||
|
@ -384,15 +398,19 @@ export default mixins(
|
|||
await this.restApi().deleteExecutions(sendData);
|
||||
} catch (error) {
|
||||
this.isDataLoading = false;
|
||||
this.$showError(error, 'Problem deleting executions', 'There was a problem deleting the executions:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.handleDeleteSelected.title'),
|
||||
this.$locale.baseText('executionsList.showError.handleDeleteSelected.message'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
this.isDataLoading = false;
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Execution deleted',
|
||||
message: 'The executions were deleted!',
|
||||
title: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.title'),
|
||||
message: this.$locale.baseText('executionsList.showMessage.handleDeleteSelected.message'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -543,10 +561,19 @@ export default mixins(
|
|||
data = await this.restApi().getPastExecutions(filter, this.requestItemsPerRequest, lastId);
|
||||
} catch (error) {
|
||||
this.isDataLoading = false;
|
||||
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.loadMore.title'),
|
||||
this.$locale.baseText('executionsList.showError.loadMore.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
data.results = data.results.map((execution) => {
|
||||
// @ts-ignore
|
||||
return { ...execution, mode: execution.mode };
|
||||
});
|
||||
|
||||
this.finishedExecutions.push.apply(this.finishedExecutions, data.results);
|
||||
this.finishedExecutionsCount = data.count;
|
||||
this.finishedExecutionsCountEstimated = data.estimated;
|
||||
|
@ -569,12 +596,16 @@ export default mixins(
|
|||
// @ts-ignore
|
||||
workflows.unshift({
|
||||
id: 'ALL',
|
||||
name: 'All Workflows',
|
||||
name: this.$locale.baseText('executionsList.allWorkflows'),
|
||||
});
|
||||
|
||||
Vue.set(this, 'workflows', workflows);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.loadWorkflows.title'),
|
||||
this.$locale.baseText('executionsList.showError.loadWorkflows.message') + ':',
|
||||
);
|
||||
}
|
||||
},
|
||||
async openDialog () {
|
||||
|
@ -597,21 +628,25 @@ export default mixins(
|
|||
|
||||
if (retrySuccessful === true) {
|
||||
this.$showMessage({
|
||||
title: 'Retry successful',
|
||||
message: 'The retry was successful!',
|
||||
title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.title'),
|
||||
message: this.$locale.baseText('executionsList.showMessage.retrySuccessfulTrue.message'),
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
this.$showMessage({
|
||||
title: 'Retry unsuccessful',
|
||||
message: 'The retry was not successful!',
|
||||
title: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.title'),
|
||||
message: this.$locale.baseText('executionsList.showMessage.retrySuccessfulFalse.message'),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
|
||||
this.isDataLoading = false;
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem with retry', 'There was a problem with the retry:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.retryExecution.title'),
|
||||
this.$locale.baseText('executionsList.showError.retryExecution.message') + ':',
|
||||
);
|
||||
|
||||
this.isDataLoading = false;
|
||||
}
|
||||
|
@ -624,7 +659,11 @@ export default mixins(
|
|||
const finishedExecutionsPromise = this.loadFinishedExecutions();
|
||||
await Promise.all([activeExecutionsPromise, finishedExecutionsPromise]);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading', 'There was a problem loading the data:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.refreshData.title'),
|
||||
this.$locale.baseText('executionsList.showError.refreshData.message') + ':',
|
||||
);
|
||||
}
|
||||
|
||||
this.isDataLoading = false;
|
||||
|
@ -633,23 +672,41 @@ export default mixins(
|
|||
if (entry.waitTill) {
|
||||
const waitDate = new Date(entry.waitTill);
|
||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||
return 'The workflow is waiting indefinitely for an incoming webhook call.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsWaitingIndefinitely');
|
||||
}
|
||||
return `The worklow is waiting till ${waitDate.toLocaleDateString()} ${waitDate.toLocaleTimeString()}.`;
|
||||
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowIsWaitingTill',
|
||||
{
|
||||
interpolate: {
|
||||
waitDateDate: waitDate.toLocaleDateString(),
|
||||
waitDateTime: waitDate.toLocaleTimeString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
} else if (entry.stoppedAt === undefined) {
|
||||
return 'The worklow is currently executing.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowIsCurrentlyExecuting');
|
||||
} else if (entry.finished === true && entry.retryOf !== undefined) {
|
||||
return `The workflow execution was a retry of "${entry.retryOf}" and it was successful.`;
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndItWasSuccessful',
|
||||
{ interpolate: { entryRetryOf: entry.retryOf }},
|
||||
);
|
||||
} else if (entry.finished === true) {
|
||||
return 'The worklow execution was successful.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionWasSuccessful');
|
||||
} else if (entry.retryOf !== undefined) {
|
||||
return `The workflow execution was a retry of "${entry.retryOf}" and failed.<br />New retries have to be started from the original execution.`;
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowExecutionWasARetryOfAndFailed',
|
||||
{ interpolate: { entryRetryOf: entry.retryOf }},
|
||||
);
|
||||
} else if (entry.retrySuccessId !== undefined) {
|
||||
return `The workflow execution failed but the retry "${entry.retrySuccessId}" was successful.`;
|
||||
return this.$locale.baseText(
|
||||
'executionsList.statusTooltipText.theWorkflowExecutionFailedButTheRetryWasSuccessful',
|
||||
{ interpolate: { entryRetrySuccessId: entry.retrySuccessId }},
|
||||
);
|
||||
} else if (entry.stoppedAt === null) {
|
||||
return 'The workflow execution is probably still running but it may have crashed and n8n cannot safely tell. ';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionIsProbablyStillRunning');
|
||||
} else {
|
||||
return 'The workflow execution failed.';
|
||||
return this.$locale.baseText('executionsList.statusTooltipText.theWorkflowExecutionFailed');
|
||||
}
|
||||
},
|
||||
async stopExecution (activeExecutionId: string) {
|
||||
|
@ -665,14 +722,21 @@ export default mixins(
|
|||
this.stoppingExecutions.splice(index, 1);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Execution stopped',
|
||||
message: `The execution with the id "${activeExecutionId}" got stopped!`,
|
||||
title: this.$locale.baseText('executionsList.showMessage.stopExecution.title'),
|
||||
message: this.$locale.baseText(
|
||||
'executionsList.showMessage.stopExecution.message',
|
||||
{ interpolate: { activeExecutionId } },
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
this.refreshData();
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem stopping execution', 'There was a problem stopping the execuction:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('executionsList.showError.stopExecution.title'),
|
||||
this.$locale.baseText('executionsList.showError.stopExecution.message'),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible" @keydown.stop>
|
||||
<el-dialog :visible="dialogVisible" custom-class="expression-dialog classic" append-to-body width="80%" title="Edit Expression" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" custom-class="expression-dialog classic" append-to-body width="80%" :title="$locale.baseText('expressionEdit.editExpression')" :before-close="closeDialog">
|
||||
<el-row>
|
||||
<el-col :span="8">
|
||||
<div class="header-side-menu">
|
||||
<div class="headline">
|
||||
Edit Expression
|
||||
{{ $locale.baseText('expressionEdit.editExpression') }}
|
||||
</div>
|
||||
<div class="sub-headline">
|
||||
Variable Selector
|
||||
{{ $locale.baseText('expressionEdit.variableSelector') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -19,7 +19,7 @@
|
|||
<el-col :span="16" class="right-side">
|
||||
<div class="expression-editor-wrapper">
|
||||
<div class="editor-description">
|
||||
Expression
|
||||
{{ $locale.baseText('expressionEdit.expression') }}
|
||||
</div>
|
||||
<div class="expression-editor">
|
||||
<expression-input :parameter="parameter" ref="inputFieldExpression" rows="8" :value="value" :path="path" @change="valueChanged" @keydown.stop="noOp"></expression-input>
|
||||
|
@ -28,7 +28,7 @@
|
|||
|
||||
<div class="expression-result-wrapper">
|
||||
<div class="editor-description">
|
||||
Result
|
||||
{{ $locale.baseText('expressionEdit.result') }}
|
||||
</div>
|
||||
<expression-input :parameter="parameter" resolvedValue="true" ref="expressionResult" rows="8" :value="displayValue" :path="path"></expression-input>
|
||||
</div>
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
<template>
|
||||
<div @keydown.stop class="fixed-collection-parameter">
|
||||
<div v-if="getProperties.length === 0" class="no-items-exist">
|
||||
<n8n-text size="small">Currently no items exist</n8n-text>
|
||||
<n8n-text size="small">{{ $locale.baseText('fixedCollectionParameter.currentlyNoItemsExist') }}</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-for="property in getProperties" :key="property.name" class="fixed-collection-parameter-property">
|
||||
<n8n-input-label
|
||||
:label="property.displayName === '' || parameter.options.length === 1 ? '' : property.displayName"
|
||||
:label="property.displayName === '' || parameter.options.length === 1 ? '' : $locale.nodeText().topParameterDisplayName(property)"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
size="small"
|
||||
|
@ -15,10 +15,10 @@
|
|||
<div v-for="(value, index) in values[property.name]" :key="property.name + index" class="parameter-item">
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" title="Delete Item" @click="deleteOption(property.name, index)" />
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" :title="$locale.baseText('fixedCollectionParameter.deleteItem')" @click="deleteOption(property.name, index)" />
|
||||
<div v-if="sortable" class="sort-icon">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" title="Move up" @click="moveOptionUp(property.name, index)" />
|
||||
<font-awesome-icon v-if="index !== (values[property.name].length -1)" icon="angle-down" class="clickable" title="Move down" @click="moveOptionDown(property.name, index)" />
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('fixedCollectionParameter.moveUp')" @click="moveOptionUp(property.name, index)" />
|
||||
<font-awesome-icon v-if="index !== (values[property.name].length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('fixedCollectionParameter.moveDown')" @click="moveOptionDown(property.name, index)" />
|
||||
</div>
|
||||
</div>
|
||||
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name, index)" :hideDelete="true" @valueChanged="valueChanged" />
|
||||
|
@ -28,7 +28,7 @@
|
|||
<div v-else class="parameter-item">
|
||||
<div class="parameter-item-wrapper">
|
||||
<div class="delete-option" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" title="Delete Item" @click="deleteOption(property.name)" />
|
||||
<font-awesome-icon icon="trash" class="reset-icon clickable" :title="$locale.baseText('fixedCollectionParameter.deleteItem')" @click="deleteOption(property.name)" />
|
||||
</div>
|
||||
<parameter-input-list :parameters="property.values" :nodeValues="nodeValues" :path="getPropertyPath(property.name)" class="parameter-item" @valueChanged="valueChanged" :hideDelete="true" />
|
||||
</div>
|
||||
|
@ -43,7 +43,7 @@
|
|||
<n8n-option
|
||||
v-for="item in parameterOptions"
|
||||
:key="item.name"
|
||||
:label="item.displayName"
|
||||
:label="$locale.nodeText().collectionOptionDisplayName(parameter, item)"
|
||||
:value="item.name">
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -85,7 +85,8 @@ export default mixins(genericHelpers)
|
|||
},
|
||||
computed: {
|
||||
getPlaceholderText (): string {
|
||||
return this.parameter.placeholder ? this.parameter.placeholder : 'Choose Option To Add';
|
||||
const placeholder = this.$locale.nodeText().placeholder(this.parameter);
|
||||
return placeholder ? placeholder : this.$locale.baseText('fixedCollectionParameter.choose');
|
||||
},
|
||||
getProperties (): INodePropertyCollection[] {
|
||||
const returnProperties = [];
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<span class="title">
|
||||
Execution Id:
|
||||
{{ $locale.baseText('executionDetails.executionId') + ':' }}
|
||||
<span>
|
||||
<strong>{{ executionId }}</strong
|
||||
>
|
||||
|
@ -9,23 +9,23 @@
|
|||
icon="check"
|
||||
class="execution-icon success"
|
||||
v-if="executionFinished"
|
||||
title="Execution was successful"
|
||||
:title="$locale.baseText('executionDetails.executionWasSuccessful')"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="clock"
|
||||
class="execution-icon warning"
|
||||
v-else-if="executionWaiting"
|
||||
title="Execution waiting"
|
||||
:title="$locale.baseText('executionDetails.executionWaiting')"
|
||||
/>
|
||||
<font-awesome-icon
|
||||
icon="times"
|
||||
class="execution-icon error"
|
||||
v-else
|
||||
title="Execution failed"
|
||||
:title="$locale.baseText('executionDetails.executionFailed')"
|
||||
/>
|
||||
</span>
|
||||
of
|
||||
<span class="primary-color clickable" title="Open Workflow">
|
||||
{{ $locale.baseText('executionDetails.of') }}
|
||||
<span class="primary-color clickable" :title="$locale.baseText('executionDetails.openWorkflow')">
|
||||
<WorkflowNameShort :name="workflowName">
|
||||
<template v-slot="{ shortenedName }">
|
||||
<span @click="openWorkflow(workflowExecution.workflowId)">
|
||||
|
@ -34,7 +34,7 @@
|
|||
</template>
|
||||
</WorkflowNameShort>
|
||||
</span>
|
||||
workflow
|
||||
{{ $locale.baseText('executionDetails.workflow') }}
|
||||
</span>
|
||||
<ReadOnly class="read-only" />
|
||||
</div>
|
||||
|
@ -117,4 +117,8 @@ export default mixins(titleChange).extend({
|
|||
.read-only {
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.el-tooltip.read-only div {
|
||||
max-width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,13 +1,25 @@
|
|||
<template>
|
||||
<n8n-tooltip class="primary-color" placement="bottom-end" >
|
||||
<div slot="content">
|
||||
You're viewing the log of a previous execution. You cannot<br />
|
||||
make changes since this execution already occured. Make changes<br />
|
||||
to this workflow by clicking on its name on the left.
|
||||
<span v-html="$locale.baseText('executionDetails.readOnly.youreViewingTheLogOf')"></span>
|
||||
</div>
|
||||
<span>
|
||||
<div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
Read only
|
||||
</span>
|
||||
<span v-html="$locale.baseText('executionDetails.readOnly.readOnly')"></span>
|
||||
</div>
|
||||
</n8n-tooltip>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: "ReadOnly",
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
svg {
|
||||
margin-right: 6px;
|
||||
}
|
||||
</style>
|
|
@ -33,7 +33,7 @@
|
|||
@blur="onTagsBlur"
|
||||
@update="onTagsUpdate"
|
||||
@esc="onTagsEditEsc"
|
||||
placeholder="Choose or create a tag"
|
||||
:placeholder="$locale.baseText('workflowDetails.chooseOrCreateATag')"
|
||||
ref="dropdown"
|
||||
class="tags-edit"
|
||||
/>
|
||||
|
@ -46,7 +46,7 @@
|
|||
class="add-tag clickable"
|
||||
@click="onTagsEditEnable"
|
||||
>
|
||||
+ Add tag
|
||||
+ {{ $locale.baseText('workflowDetails.addTag') }}
|
||||
</span>
|
||||
</div>
|
||||
<TagsContainer
|
||||
|
@ -62,7 +62,7 @@
|
|||
<PushConnectionTracker class="actions">
|
||||
<template>
|
||||
<span class="activator">
|
||||
<span>Active:</span>
|
||||
<span>{{ $locale.baseText('workflowDetails.active') + ':' }}</span>
|
||||
<WorkflowActivator :workflow-active="isWorkflowActive" :workflow-id="currentWorkflowId" :disabled="!currentWorkflowId"/>
|
||||
</span>
|
||||
<SaveButton
|
||||
|
@ -197,8 +197,8 @@ export default mixins(workflowHelpers).extend({
|
|||
const newName = name.trim();
|
||||
if (!newName) {
|
||||
this.$showMessage({
|
||||
title: "Name missing",
|
||||
message: `Please enter a name, or press 'esc' to go back to the old one.`,
|
||||
title: this.$locale.baseText('workflowDetails.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowDetails.showMessage.message'),
|
||||
type: "error",
|
||||
});
|
||||
|
||||
|
|
|
@ -22,94 +22,94 @@
|
|||
<el-submenu index="workflow" title="Workflow" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="network-wired"/>
|
||||
<span slot="title" class="item-title-root">Workflows</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.workflows') }}</span>
|
||||
</template>
|
||||
|
||||
<n8n-menu-item index="workflow-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">New</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.new') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.open') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-save">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="save"/>
|
||||
<span slot="title" class="item-title">Save</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.save') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-duplicate" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="copy"/>
|
||||
<span slot="title" class="item-title">Duplicate</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.duplicate') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-delete" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="trash"/>
|
||||
<span slot="title" class="item-title">Delete</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.delete') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-download">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file-download"/>
|
||||
<span slot="title" class="item-title">Download</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.download') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-url">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cloud"/>
|
||||
<span slot="title" class="item-title">Import from URL</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.importFromUrl') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-import-file">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="hdd"/>
|
||||
<span slot="title" class="item-title">Import from File</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.importFromFile') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="workflow-settings" :disabled="!currentWorkflow">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="cog"/>
|
||||
<span slot="title" class="item-title">Settings</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.settings') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<el-submenu index="credentials" title="Credentials" popperClass="sidebar-popper">
|
||||
<el-submenu index="credentials" :title="$locale.baseText('mainSidebar.credentials')" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="key"/>
|
||||
<span slot="title" class="item-title-root">Credentials</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.credentials') }}</span>
|
||||
</template>
|
||||
|
||||
<n8n-menu-item index="credentials-new">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="file"/>
|
||||
<span slot="title" class="item-title">New</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.new') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
<n8n-menu-item index="credentials-open">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="folder-open"/>
|
||||
<span slot="title" class="item-title">Open</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.open') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
||||
<n8n-menu-item index="executions">
|
||||
<font-awesome-icon icon="tasks"/>
|
||||
<span slot="title" class="item-title-root">Executions</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.executions') }}</span>
|
||||
</n8n-menu-item>
|
||||
|
||||
<el-submenu index="help" class="help-menu" title="Help" popperClass="sidebar-popper">
|
||||
<template slot="title">
|
||||
<font-awesome-icon icon="question"/>
|
||||
<span slot="title" class="item-title-root">Help</span>
|
||||
<span slot="title" class="item-title-root">{{ $locale.baseText('mainSidebar.help') }}</span>
|
||||
</template>
|
||||
|
||||
<MenuItemsIterator :items="helpMenuItems" :afterItemClick="trackHelpItemClick" />
|
||||
|
@ -117,7 +117,7 @@
|
|||
<n8n-menu-item index="help-about">
|
||||
<template slot="title">
|
||||
<font-awesome-icon class="about-icon" icon="info"/>
|
||||
<span slot="title" class="item-title">About n8n</span>
|
||||
<span slot="title" class="item-title">{{ $locale.baseText('mainSidebar.aboutN8n') }}</span>
|
||||
</template>
|
||||
</n8n-menu-item>
|
||||
</el-submenu>
|
||||
|
@ -168,39 +168,6 @@ import { mapGetters } from 'vuex';
|
|||
import MenuItemsIterator from './MainSidebarMenuItemsIterator.vue';
|
||||
import { CREDENTIAL_LIST_MODAL_KEY, CREDENTIAL_SELECT_MODAL_KEY, DUPLICATE_MODAL_KEY, TAGS_MANAGER_MODAL_KEY, VERSIONS_MODAL_KEY, WORKFLOW_SETTINGS_MODAL_KEY, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||
|
||||
const helpMenuItems: IMenuItem[] = [
|
||||
{
|
||||
id: 'docs',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io',
|
||||
title: 'Documentation',
|
||||
icon: 'book',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://community.n8n.io',
|
||||
title: 'Forum',
|
||||
icon: 'users',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://n8n.io/workflows',
|
||||
title: 'Workflows',
|
||||
icon: 'network-wired',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
restApi,
|
||||
|
@ -225,7 +192,6 @@ export default mixins(
|
|||
basePath: this.$store.getters.getBaseUrl,
|
||||
executionsListDialogVisible: false,
|
||||
stopExecutionInProgress: false,
|
||||
helpMenuItems,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -236,6 +202,40 @@ export default mixins(
|
|||
'hasVersionUpdates',
|
||||
'nextVersions',
|
||||
]),
|
||||
helpMenuItems (): object[] {
|
||||
return [
|
||||
{
|
||||
id: 'docs',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://docs.n8n.io',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.documentation'),
|
||||
icon: 'book',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'forum',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://community.n8n.io',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.forum'),
|
||||
icon: 'users',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'examples',
|
||||
type: 'link',
|
||||
properties: {
|
||||
href: 'https://n8n.io/workflows',
|
||||
title: this.$locale.baseText('mainSidebar.helpMenuItems.workflows'),
|
||||
icon: 'network-wired',
|
||||
newWindow: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
exeuctionId (): string | undefined {
|
||||
return this.$route.params.id;
|
||||
},
|
||||
|
@ -322,12 +322,19 @@ export default mixins(
|
|||
this.stopExecutionInProgress = true;
|
||||
await this.restApi().stopCurrentExecution(executionId);
|
||||
this.$showMessage({
|
||||
title: 'Execution stopped',
|
||||
message: `The execution with the id "${executionId}" got stopped!`,
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.stopExecution.title'),
|
||||
message: this.$locale.baseText(
|
||||
'mainSidebar.showMessage.stopExecution.message',
|
||||
{ interpolate: { executionId }},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem stopping execution', 'There was a problem stopping the execuction:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.title'),
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.message') + ':',
|
||||
);
|
||||
}
|
||||
this.stopExecutionInProgress = false;
|
||||
},
|
||||
|
@ -351,8 +358,8 @@ export default mixins(
|
|||
worflowData = JSON.parse(data as string);
|
||||
} catch (error) {
|
||||
this.$showMessage({
|
||||
title: 'Could not import file',
|
||||
message: `The file does not contain valid JSON data.`,
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleFileImport.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleFileImport.message'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
|
@ -374,17 +381,30 @@ export default mixins(
|
|||
(this.$refs.importFile as HTMLInputElement).click();
|
||||
} else if (key === 'workflow-import-url') {
|
||||
try {
|
||||
const promptResponse = await this.$prompt(`Workflow URL:`, 'Import Workflow from URL:', {
|
||||
confirmButtonText: 'Import',
|
||||
cancelButtonText: 'Cancel',
|
||||
inputErrorMessage: 'Invalid URL',
|
||||
inputPattern: /^http[s]?:\/\/.*\.json$/i,
|
||||
}) as MessageBoxInputData;
|
||||
const promptResponse = await this.$prompt(
|
||||
this.$locale.baseText('mainSidebar.prompt.workflowUrl') + ':',
|
||||
this.$locale.baseText('mainSidebar.prompt.importWorkflowFromUrl') + ':',
|
||||
{
|
||||
confirmButtonText: this.$locale.baseText('mainSidebar.prompt.import'),
|
||||
cancelButtonText: this.$locale.baseText('mainSidebar.prompt.cancel'),
|
||||
inputErrorMessage: this.$locale.baseText('mainSidebar.prompt.invalidUrl'),
|
||||
inputPattern: /^http[s]?:\/\/.*\.json$/i,
|
||||
},
|
||||
) as MessageBoxInputData;
|
||||
|
||||
this.$root.$emit('importWorkflowUrl', { url: promptResponse.value });
|
||||
} catch (e) {}
|
||||
} else if (key === 'workflow-delete') {
|
||||
const deleteConfirmed = await this.confirmMessage(`Are you sure that you want to delete the workflow "${this.workflowName}"?`, 'Delete Workflow?', 'warning', 'Yes, delete!');
|
||||
const deleteConfirmed = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'mainSidebar.confirmMessage.workflowDelete.message',
|
||||
{ interpolate: { workflowName: this.workflowName } },
|
||||
),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.confirmButtonText'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowDelete.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (deleteConfirmed === false) {
|
||||
return;
|
||||
|
@ -393,15 +413,22 @@ export default mixins(
|
|||
try {
|
||||
await this.restApi().deleteWorkflow(this.currentWorkflow);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem deleting the workflow', 'There was a problem deleting the workflow:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.title'),
|
||||
this.$locale.baseText('mainSidebar.showError.stopExecution.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.$store.commit('setStateDirty', false);
|
||||
// Reset tab title since workflow is deleted.
|
||||
this.$titleReset();
|
||||
this.$showMessage({
|
||||
title: 'Workflow was deleted',
|
||||
message: `The workflow "${this.workflowName}" was deleted!`,
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect1.title'),
|
||||
message: this.$locale.baseText(
|
||||
'mainSidebar.showMessage.handleSelect1.message',
|
||||
{ interpolate: { workflowName: this.workflowName }},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
@ -437,7 +464,13 @@ export default mixins(
|
|||
} else if (key === 'workflow-new') {
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.message'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.confirmButtonText'),
|
||||
this.$locale.baseText('mainSidebar.confirmMessage.workflowNew.cancelButtonText'),
|
||||
);
|
||||
if (importConfirm === true) {
|
||||
this.$store.commit('setStateDirty', false);
|
||||
if (this.$router.currentRoute.name === 'NodeViewNew') {
|
||||
|
@ -447,8 +480,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow created',
|
||||
message: 'A new workflow got created!',
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleSelect2.message'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
@ -458,8 +491,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow created',
|
||||
message: 'A new workflow got created!',
|
||||
title: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.title'),
|
||||
message: this.$locale.baseText('mainSidebar.showMessage.handleSelect3.message'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div @keydown.stop class="duplicate-parameter">
|
||||
<n8n-input-label
|
||||
:label="parameter.displayName"
|
||||
:tooltipText="parameter.description"
|
||||
:label="$locale.nodeText().topParameterDisplayName(parameter)"
|
||||
:tooltipText="$locale.nodeText().topParameterDescription(parameter)"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
size="small"
|
||||
|
@ -10,10 +10,10 @@
|
|||
|
||||
<div v-for="(value, index) in values" :key="index" class="duplicate-parameter-item" :class="parameter.type">
|
||||
<div class="delete-item clickable" v-if="!isReadOnly">
|
||||
<font-awesome-icon icon="trash" title="Delete Item" @click="deleteItem(index)" />
|
||||
<font-awesome-icon icon="trash" :title="$locale.baseText('multipleParameter.deleteItem')" @click="deleteItem(index)" />
|
||||
<div v-if="sortable">
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" title="Move up" @click="moveOptionUp(index)" />
|
||||
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" title="Move down" @click="moveOptionDown(index)" />
|
||||
<font-awesome-icon v-if="index !== 0" icon="angle-up" class="clickable" :title="$locale.baseText('multipleParameter.moveUp')" @click="moveOptionUp(index)" />
|
||||
<font-awesome-icon v-if="index !== (values.length -1)" icon="angle-down" class="clickable" :title="$locale.baseText('multipleParameter.moveDown')" @click="moveOptionDown(index)" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="parameter.type === 'collection'">
|
||||
|
@ -26,7 +26,7 @@
|
|||
|
||||
<div class="add-item-wrapper">
|
||||
<div v-if="values && Object.keys(values).length === 0 || isReadOnly" class="no-items-exist">
|
||||
<n8n-text size="small">Currently no items exist</n8n-text>
|
||||
<n8n-text size="small">{{ $locale.baseText('multipleParameter.currentlyNoItemsExist') }}</n8n-text>
|
||||
</div>
|
||||
<n8n-button v-if="!isReadOnly" fullWidth @click="addItem()" :label="addButtonText" />
|
||||
</div>
|
||||
|
@ -64,7 +64,14 @@ export default mixins(genericHelpers)
|
|||
],
|
||||
computed: {
|
||||
addButtonText (): string {
|
||||
return (this.parameter.typeOptions && this.parameter.typeOptions.multipleValueButtonText) ? this.parameter.typeOptions.multipleValueButtonText : 'Add item';
|
||||
if (
|
||||
!this.parameter.typeOptions &&
|
||||
!this.parameter.typeOptions.multipleValueButtonText
|
||||
) {
|
||||
return this.$locale.baseText('multipleParameter.addItem');
|
||||
}
|
||||
|
||||
return this.$locale.nodeText().multipleValueButtonText(this.parameter);
|
||||
},
|
||||
hideDelete (): boolean {
|
||||
return this.parameter.options.length === 1;
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
</span>
|
||||
</div>
|
||||
|
||||
<div class="node-executing-info" title="Node is executing">
|
||||
<div class="node-executing-info" :title="$locale.baseText('node.nodeIsExecuting')">
|
||||
<font-awesome-icon icon="sync-alt" spin />
|
||||
</div>
|
||||
|
||||
|
@ -37,31 +37,36 @@
|
|||
</div>
|
||||
|
||||
<div class="node-options no-select-on-click" v-if="!isReadOnly" v-show="!hideActions">
|
||||
<div v-touch:tap="deleteNode" class="option" title="Delete Node" >
|
||||
<div v-touch:tap="deleteNode" class="option" :title="$locale.baseText('node.deleteNode')" >
|
||||
|
||||
<font-awesome-icon icon="trash" />
|
||||
</div>
|
||||
<div v-touch:tap="disableNode" class="option" title="Activate/Deactivate Node" >
|
||||
<div v-touch:tap="disableNode" class="option" :title="$locale.baseText('node.activateDeactivateNode')">
|
||||
<font-awesome-icon :icon="nodeDisabledIcon" />
|
||||
</div>
|
||||
<div v-touch:tap="duplicateNode" class="option" title="Duplicate Node" >
|
||||
<div v-touch:tap="duplicateNode" class="option" :title="$locale.baseText('node.duplicateNode')">
|
||||
<font-awesome-icon icon="clone" />
|
||||
</div>
|
||||
<div v-touch:tap="setNodeActive" class="option touch" title="Edit Node" v-if="!isReadOnly">
|
||||
<div v-touch:tap="setNodeActive" class="option touch" :title="$locale.baseText('node.editNode')" v-if="!isReadOnly">
|
||||
<font-awesome-icon class="execute-icon" icon="cog" />
|
||||
</div>
|
||||
<div v-touch:tap="executeNode" class="option" title="Execute Node" v-if="!isReadOnly && !workflowRunning">
|
||||
<div v-touch:tap="executeNode" class="option" :title="$locale.baseText('node.executeNode')" v-if="!isReadOnly && !workflowRunning">
|
||||
<font-awesome-icon class="execute-icon" icon="play-circle" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="{'disabled-linethrough': true, success: workflowDataItems > 0}" v-if="showDisabledLinethrough"></div>
|
||||
</div>
|
||||
<div class="node-description">
|
||||
<div class="node-name" :title="data.name">
|
||||
<p>{{ nodeTitle }}</p>
|
||||
<p v-if="data.disabled">(Disabled)</p>
|
||||
<div class="node-name" :title="nodeTitle">
|
||||
<p>
|
||||
{{ nodeTitle }}
|
||||
</p>
|
||||
<p v-if="data.disabled">
|
||||
({{ $locale.baseText('node.disabled') }})
|
||||
</p>
|
||||
</div>
|
||||
<div v-if="nodeSubtitle !== undefined" class="node-subtitle" :title="nodeSubtitle">
|
||||
{{nodeSubtitle}}
|
||||
{{ nodeSubtitle }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -166,7 +171,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
const nodeIssues = NodeHelpers.nodeIssuesToString(this.data.issues, this.data);
|
||||
|
||||
return 'Issues:<br /> - ' + nodeIssues.join('<br /> - ');
|
||||
return `${this.$locale.baseText('node.issues')}:<br /> - ` + nodeIssues.join('<br /> - ');
|
||||
},
|
||||
nodeDisabledIcon (): string {
|
||||
if (this.data.disabled === false) {
|
||||
|
@ -191,7 +196,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
|
||||
return returnStyles;
|
||||
},
|
||||
shortNodeType (): string {
|
||||
return this.$locale.shortNodeType(this.data.type);
|
||||
},
|
||||
nodeTitle (): string {
|
||||
if (this.data.name === 'Start') {
|
||||
return this.$locale.headerText({
|
||||
key: `headers.start.displayName`,
|
||||
fallback: 'Start',
|
||||
});
|
||||
}
|
||||
|
||||
return this.data.name;
|
||||
},
|
||||
waiting (): string | undefined {
|
||||
|
@ -202,9 +217,17 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
if (this.name === lastNodeExecuted) {
|
||||
const waitDate = new Date(workflowExecution.waitTill);
|
||||
if (waitDate.toISOString() === WAIT_TIME_UNLIMITED) {
|
||||
return 'The node is waiting indefinitely for an incoming webhook call.';
|
||||
return this.$locale.baseText('node.theNodeIsWaitingIndefinitelyForAnIncomingWebhookCall');
|
||||
}
|
||||
return `Node is waiting till ${waitDate.toLocaleDateString()} ${waitDate.toLocaleTimeString()}`;
|
||||
return this.$locale.baseText(
|
||||
'node.nodeIsWaitingTill',
|
||||
{
|
||||
interpolate: {
|
||||
date: waitDate.toLocaleDateString(),
|
||||
time: waitDate.toLocaleTimeString(),
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -325,6 +348,7 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
|||
this.$emit('duplicateNode', this.data.name);
|
||||
});
|
||||
},
|
||||
|
||||
setNodeActive () {
|
||||
this.$store.commit('setActiveNode', this.data.name);
|
||||
},
|
||||
|
|
|
@ -1,19 +1,36 @@
|
|||
<template functional>
|
||||
<template>
|
||||
<div :class="$style.category">
|
||||
<span :class="$style.name">{{ props.item.category }}</span>
|
||||
<span :class="$style.name">
|
||||
{{ renderCategoryName(categoryName) }}
|
||||
</span>
|
||||
<font-awesome-icon
|
||||
:class="$style.arrow"
|
||||
icon="chevron-down"
|
||||
v-if="props.item.properties.expanded"
|
||||
v-if="item.properties.expanded"
|
||||
/>
|
||||
<font-awesome-icon :class="$style.arrow" icon="chevron-up" v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
import Vue from 'vue';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['item'],
|
||||
};
|
||||
computed: {
|
||||
categoryName() {
|
||||
return camelcase(this.item.category);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
renderCategoryName(categoryName: string) {
|
||||
const key = `nodeCreator.categoryNames.${categoryName}`;
|
||||
|
||||
return this.$locale.exists(key) ? this.$locale.baseText(key) : categoryName;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -11,9 +11,9 @@
|
|||
/>
|
||||
<div class="type-selector">
|
||||
<el-tabs v-model="selectedType" stretch>
|
||||
<el-tab-pane label="All" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane label="Regular" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane label="Trigger" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$locale.baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div v-if="searchFilter.length === 0" class="scrollable">
|
||||
|
|
|
@ -4,27 +4,31 @@
|
|||
<NoResultsIcon />
|
||||
</div>
|
||||
<div class="title">
|
||||
<div>We didn't make that... yet</div>
|
||||
<div>
|
||||
{{ $locale.baseText('nodeCreator.noResults.weDidntMakeThatYet') }}
|
||||
</div>
|
||||
<div class="action">
|
||||
Don’t worry, you can probably do it with the
|
||||
<a @click="selectHttpRequest">HTTP Request</a> or
|
||||
<a @click="selectWebhook">Webhook</a> node
|
||||
{{ $locale.baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
||||
<a @click="selectHttpRequest">{{ $locale.baseText('nodeCreator.noResults.httpRequest') }}</a> or
|
||||
<a @click="selectWebhook">{{ $locale.baseText('nodeCreator.noResults.webhook') }}</a> {{ $locale.baseText('nodeCreator.noResults.node') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request">
|
||||
<div>Want us to make it faster?</div>
|
||||
<div>
|
||||
{{ $locale.baseText('nodeCreator.noResults.wantUsToMakeItFaster') }}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
:href="REQUEST_NODE_FORM_URL"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Request the node</span>
|
||||
<span>{{ $locale.baseText('nodeCreator.noResults.requestTheNode') }}</span>
|
||||
<span>
|
||||
<font-awesome-icon
|
||||
class="external"
|
||||
icon="external-link-alt"
|
||||
title="Request the node"
|
||||
:title="$locale.baseText('nodeCreator.noResults.requestTheNode')"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -37,7 +41,6 @@
|
|||
<script lang="ts">
|
||||
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
import NoResultsIcon from './NoResultsIcon.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
|
|
|
@ -1,15 +1,25 @@
|
|||
<template functional>
|
||||
<div :class="{[$style['node-item']]: true, [$style.bordered]: props.bordered}">
|
||||
<NodeIcon :class="$style['node-icon']" :nodeType="props.nodeType" />
|
||||
<template>
|
||||
<div :class="{[$style['node-item']]: true, [$style.bordered]: bordered}">
|
||||
<NodeIcon :class="$style['node-icon']" :nodeType="nodeType" />
|
||||
<div>
|
||||
<div :class="$style.details">
|
||||
<span :class="$style.name">{{props.nodeType.displayName}}</span>
|
||||
<span :class="$style.name">
|
||||
{{ $locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: nodeType.displayName,
|
||||
})
|
||||
}}
|
||||
</span>
|
||||
<span :class="$style['trigger-icon']">
|
||||
<TriggerIcon v-if="$options.isTrigger(props.nodeType)" />
|
||||
<TriggerIcon v-if="$options.isTrigger(nodeType)" />
|
||||
</span>
|
||||
</div>
|
||||
<div :class="$style.description">
|
||||
{{props.nodeType.description}}
|
||||
{{ $locale.headerText({
|
||||
key: `headers.${shortNodeType}.description`,
|
||||
fallback: nodeType.description,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -26,17 +36,24 @@ import TriggerIcon from '../TriggerIcon.vue';
|
|||
Vue.component('NodeIcon', NodeIcon);
|
||||
Vue.component('TriggerIcon', TriggerIcon);
|
||||
|
||||
export default {
|
||||
export default Vue.extend({
|
||||
name: 'NodeItem',
|
||||
props: [
|
||||
'active',
|
||||
'filter',
|
||||
'nodeType',
|
||||
'bordered',
|
||||
],
|
||||
computed: {
|
||||
shortNodeType() {
|
||||
return this.$locale.shortNodeType(this.nodeType.name);
|
||||
},
|
||||
},
|
||||
// @ts-ignore
|
||||
isTrigger (nodeType: INodeTypeDescription): boolean {
|
||||
return nodeType.group.includes('trigger');
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
</div>
|
||||
<div class="text">
|
||||
<input
|
||||
placeholder="Search nodes..."
|
||||
:placeholder="$locale.baseText('nodeCreator.searchBar.searchNodes')"
|
||||
ref="input"
|
||||
:value="value"
|
||||
@input="onInput"
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
<template functional>
|
||||
<template>
|
||||
<div :class="$style.subcategory">
|
||||
<div :class="$style.details">
|
||||
<div :class="$style.title">{{ props.item.properties.subcategory }}</div>
|
||||
<div v-if="props.item.properties.description" :class="$style.description">
|
||||
{{ props.item.properties.description }}
|
||||
<div :class="$style.title">
|
||||
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
||||
</div>
|
||||
<div v-if="item.properties.description" :class="$style.description">
|
||||
{{ $locale.baseText(`nodeCreator.subcategoryDescriptions.${subcategoryDescription}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
|
@ -13,9 +15,21 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
import Vue from 'vue';
|
||||
import camelcase from 'lodash.camelcase';
|
||||
|
||||
export default Vue.extend({
|
||||
props: ['item'],
|
||||
};
|
||||
computed: {
|
||||
subcategoryName() {
|
||||
return camelcase(this.item.properties.subcategory);
|
||||
},
|
||||
subcategoryDescription() {
|
||||
const firstWord = this.item.properties.description.split(' ').shift() || '';
|
||||
return firstWord.toLowerCase().replace(/,/g, '');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,9 @@
|
|||
<div class="clickable" @click="onBackArrowClick">
|
||||
<font-awesome-icon class="back-arrow" icon="arrow-left" />
|
||||
</div>
|
||||
<span>{{ title }}</span>
|
||||
<span>
|
||||
{{ $locale.baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="scrollable">
|
||||
|
@ -18,6 +20,7 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { INodeCreateElement } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
|
||||
|
@ -29,6 +32,11 @@ export default Vue.extend({
|
|||
ItemIterator,
|
||||
},
|
||||
props: ['title', 'elements', 'activeIndex'],
|
||||
computed: {
|
||||
subcategoryName() {
|
||||
return camelcase(this.title);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selected(element: INodeCreateElement) {
|
||||
this.$emit('selected', element);
|
||||
|
|
|
@ -2,7 +2,14 @@
|
|||
<div v-if="credentialTypesNodeDescriptionDisplayed.length" :class="$style.container">
|
||||
<div v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" :key="credentialTypeDescription.name">
|
||||
<n8n-input-label
|
||||
:label="`Credential for ${credentialTypeNames[credentialTypeDescription.name]}`"
|
||||
:label="$locale.baseText(
|
||||
'nodeCredentials.credentialFor',
|
||||
{
|
||||
interpolate: {
|
||||
credentialType: credentialTypeNames[credentialTypeDescription.name]
|
||||
}
|
||||
}
|
||||
)"
|
||||
:bold="false"
|
||||
size="small"
|
||||
|
||||
|
@ -13,7 +20,7 @@
|
|||
</div>
|
||||
|
||||
<div :class="issues.length ? $style.hasIssues : $style.input" v-else >
|
||||
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" placeholder="Select Credential" size="small">
|
||||
<n8n-select :value="getSelectedId(credentialTypeDescription.name)" @change="(value) => onCredentialSelected(credentialTypeDescription.name, value)" :placeholder="$locale.baseText('nodeCredentials.selectCredential')" size="small">
|
||||
<n8n-option
|
||||
v-for="(item) in credentialOptions[credentialTypeDescription.name]"
|
||||
:key="item.id"
|
||||
|
@ -30,13 +37,13 @@
|
|||
|
||||
<div :class="$style.warning" v-if="issues.length">
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="'Issues:<br /> - ' + issues.join('<br /> - ')"></div>
|
||||
<div slot="content" v-html="`${$locale.baseText('nodeCredentials.issues')}:<br /> - ` + issues.join('<br /> - ')"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
||||
<div :class="$style.edit" v-if="selected[credentialTypeDescription.name] && isCredentialExisting(credentialTypeDescription.name)">
|
||||
<font-awesome-icon icon="pen" @click="editCredential(credentialTypeDescription.name)" class="clickable" title="Update Credentials" />
|
||||
<font-awesome-icon icon="pen" @click="editCredential(credentialTypeDescription.name)" class="clickable" :title="$locale.baseText('nodeCredentials.updateCredential')" />
|
||||
</div>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
|
@ -66,8 +73,6 @@ import { mapGetters } from "vuex";
|
|||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
const NEW_CREDENTIALS_TEXT = '- Create New -';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
nodeHelpers,
|
||||
|
@ -80,7 +85,7 @@ export default mixins(
|
|||
],
|
||||
data () {
|
||||
return {
|
||||
NEW_CREDENTIALS_TEXT,
|
||||
NEW_CREDENTIALS_TEXT: `- ${this.$locale.baseText('nodeCredentials.createNew')} -`,
|
||||
newCredentialUnsubscribe: null as null | (() => void),
|
||||
};
|
||||
},
|
||||
|
@ -186,7 +191,7 @@ export default mixins(
|
|||
},
|
||||
|
||||
onCredentialSelected (credentialType: string, credentialId: string | null | undefined) {
|
||||
if (credentialId === NEW_CREDENTIALS_TEXT) {
|
||||
if (credentialId === this.NEW_CREDENTIALS_TEXT) {
|
||||
this.listenForNewCredentials(credentialType);
|
||||
this.$store.dispatch('ui/openNewCredential', { type: credentialType });
|
||||
this.$telemetry.track('User opened Credential modal', { credential_type: credentialType, source: 'node', new_credential: true, workflow_id: this.$store.getters.workflowId });
|
||||
|
@ -210,8 +215,16 @@ export default mixins(
|
|||
});
|
||||
this.updateNodesCredentialsIssues();
|
||||
this.$showMessage({
|
||||
title: 'Node credentials updated',
|
||||
message: `Nodes that used credentials "${oldCredentials.name}" have been updated to use "${selected.name}"`,
|
||||
title: this.$locale.baseText('nodeCredentials.showMessage.title'),
|
||||
message: this.$locale.baseText(
|
||||
'nodeCredentials.showMessage.message',
|
||||
{
|
||||
interpolate: {
|
||||
oldCredentialName: oldCredentials.name,
|
||||
newCredentialName: selected.name,
|
||||
},
|
||||
},
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,26 +5,35 @@
|
|||
<display-with-change :key-name="'name'" @valueChanged="valueChanged"></display-with-change>
|
||||
<a v-if="nodeType" :href="'http://n8n.io/nodes/' + nodeType.name" target="_blank" class="node-info">
|
||||
<n8n-tooltip class="clickable" placement="top" >
|
||||
<div slot="content" v-html="'<strong>Node Description:</strong><br />' + nodeTypeDescription + '<br /><br /><strong>Click the \'?\' icon to open this node on n8n.io </strong>'"></div>
|
||||
<div slot="content" v-html="`<strong>${$locale.baseText('nodeSettings.nodeDescription')}:</strong><br />` + nodeTypeDescription + `<br /><br /><strong>${$locale.baseText('nodeSettings.clickOnTheQuestionMarkIcon')}</strong>`"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div class="node-is-not-valid" v-if="node && !nodeValid">
|
||||
<n8n-text>The node is not valid as its type "{{node.type}}" is unknown.</n8n-text>
|
||||
<n8n-text>
|
||||
{{
|
||||
$locale.baseText(
|
||||
'nodeSettings.theNodeIsNotValidAsItsTypeIsUnknown',
|
||||
{ interpolate: { nodeType: node.type } },
|
||||
)
|
||||
}}
|
||||
</n8n-text>
|
||||
</div>
|
||||
<div class="node-parameters-wrapper" v-if="node && nodeValid">
|
||||
<el-tabs stretch @tab-click="handleTabClick">
|
||||
<el-tab-pane label="Parameters">
|
||||
<el-tab-pane :label="$locale.baseText('nodeSettings.parameters')">
|
||||
<node-credentials :node="node" @credentialSelected="credentialSelected"></node-credentials>
|
||||
<node-webhooks :node="node" :nodeType="nodeType" />
|
||||
<parameter-input-list :parameters="parametersNoneSetting" :hideDelete="true" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
|
||||
<div v-if="parametersNoneSetting.length === 0" class="no-parameters">
|
||||
<n8n-text>This node does not have any parameters.</n8n-text>
|
||||
<n8n-text>
|
||||
{{ $locale.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="Settings">
|
||||
<el-tab-pane :label="$locale.baseText('nodeSettings.settings')">
|
||||
<parameter-input-list :parameters="nodeSettings" :hideDelete="true" :nodeValues="nodeValues" path="" @valueChanged="valueChanged" />
|
||||
<parameter-input-list :parameters="parametersSetting" :nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged" />
|
||||
</el-tab-pane>
|
||||
|
@ -86,11 +95,28 @@ export default mixins(
|
|||
|
||||
return null;
|
||||
},
|
||||
nodeTypeName(): string {
|
||||
if (this.nodeType) {
|
||||
const shortNodeType = this.$locale.shortNodeType(this.nodeType.name);
|
||||
|
||||
return this.$locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: this.nodeType.name,
|
||||
});
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
nodeTypeDescription (): string {
|
||||
if (this.nodeType && this.nodeType.description) {
|
||||
return this.nodeType.description;
|
||||
const shortNodeType = this.$locale.shortNodeType(this.nodeType.name);
|
||||
|
||||
return this.$locale.headerText({
|
||||
key: `headers.${shortNodeType}.description`,
|
||||
fallback: this.nodeType.description,
|
||||
});
|
||||
} else {
|
||||
return 'No description found';
|
||||
return this.$locale.baseText('nodeSettings.noDescriptionFound');
|
||||
}
|
||||
},
|
||||
headerStyle (): object {
|
||||
|
@ -145,7 +171,7 @@ export default mixins(
|
|||
|
||||
nodeSettings: [
|
||||
{
|
||||
displayName: 'Notes',
|
||||
displayName: this.$locale.baseText('nodeSettings.notes.displayName'),
|
||||
name: 'notes',
|
||||
type: 'string',
|
||||
typeOptions: {
|
||||
|
@ -153,42 +179,42 @@ export default mixins(
|
|||
},
|
||||
default: '',
|
||||
noDataExpression: true,
|
||||
description: 'Optional note to save with the node.',
|
||||
description: this.$locale.baseText('nodeSettings.notes.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Display note in flow?',
|
||||
displayName: this.$locale.baseText('nodeSettings.notesInFlow.displayName'),
|
||||
name: 'notesInFlow',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If active, the note above will display in the flow as a subtitle.',
|
||||
description: this.$locale.baseText('nodeSettings.notesInFlow.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Always Output Data',
|
||||
displayName: this.$locale.baseText('nodeSettings.alwaysOutputData.displayName'),
|
||||
name: 'alwaysOutputData',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If active, the node will return an empty item even if the <br />node returns no data during an initial execution. Be careful setting <br />this on IF-Nodes as it could cause an infinite loop.',
|
||||
description: this.$locale.baseText('nodeSettings.alwaysOutputData.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Execute Once',
|
||||
displayName: this.$locale.baseText('nodeSettings.executeOnce.displayName'),
|
||||
name: 'executeOnce',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If active, the node executes only once, with data<br /> from the first item it recieves. ',
|
||||
description: this.$locale.baseText('nodeSettings.executeOnce.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Retry On Fail',
|
||||
displayName: this.$locale.baseText('nodeSettings.retryOnFail.displayName'),
|
||||
name: 'retryOnFail',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If active, the node tries to execute a failed attempt <br /> multiple times until it succeeds.',
|
||||
description: this.$locale.baseText('nodeSettings.retryOnFail.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Max. Tries',
|
||||
displayName: this.$locale.baseText('nodeSettings.maxTries.displayName'),
|
||||
name: 'maxTries',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
|
@ -204,10 +230,10 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
noDataExpression: true,
|
||||
description: 'Number of times Retry On Fail should attempt to execute the node <br />before stopping and returning the execution as failed.',
|
||||
description: this.$locale.baseText('nodeSettings.maxTries.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Wait Between Tries',
|
||||
displayName: this.$locale.baseText('nodeSettings.waitBetweenTries.displayName'),
|
||||
name: 'waitBetweenTries',
|
||||
type: 'number',
|
||||
typeOptions: {
|
||||
|
@ -223,15 +249,15 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
noDataExpression: true,
|
||||
description: 'How long to wait between each attempt. Value in ms.',
|
||||
description: this.$locale.baseText('nodeSettings.waitBetweenTries.description'),
|
||||
},
|
||||
{
|
||||
displayName: 'Continue On Fail',
|
||||
displayName: this.$locale.baseText('nodeSettings.continueOnFail.displayName'),
|
||||
name: 'continueOnFail',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
noDataExpression: true,
|
||||
description: 'If active, the workflow continues even if this node\'s <br />execution fails. When this occurs, the node passes along input data from<br />previous nodes - so your workflow should account for unexpected output data.',
|
||||
description: this.$locale.baseText('nodeSettings.continueOnFail.description'),
|
||||
},
|
||||
] as INodeProperties[],
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div v-if="webhooksNode.length" class="webhoooks">
|
||||
<div class="clickable headline" :class="{expanded: !isMinimized}" @click="isMinimized=!isMinimized" :title="isMinimized ? 'Click to display Webhook URLs' : 'Click to hide Webhook URLs'">
|
||||
<div class="clickable headline" :class="{expanded: !isMinimized}" @click="isMinimized=!isMinimized" :title="isMinimized ? $locale.baseText('nodeWebhooks.clickToDisplayWebhookUrls') : $locale.baseText('nodeWebhooks.clickToHideWebhookUrls')">
|
||||
<font-awesome-icon icon="angle-down" class="minimize-button minimize-icon" />
|
||||
Webhook URLs
|
||||
{{ $locale.baseText('nodeWebhooks.webhookUrls') }}
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div class="node-webhooks" v-if="!isMinimized">
|
||||
|
@ -10,14 +10,14 @@
|
|||
<el-row>
|
||||
<el-col :span="24">
|
||||
<el-radio-group v-model="showUrlFor" size="mini">
|
||||
<el-radio-button label="test">Test URL</el-radio-button>
|
||||
<el-radio-button label="production">Production URL</el-radio-button>
|
||||
<el-radio-button label="test">{{ $locale.baseText('nodeWebhooks.testUrl') }}</el-radio-button>
|
||||
<el-radio-button label="production">{{ $locale.baseText('nodeWebhooks.productionUrl') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
|
||||
<n8n-tooltip v-for="(webhook, index) in webhooksNode" :key="index" class="item" content="Click to copy Webhook URL" placement="left">
|
||||
<n8n-tooltip v-for="(webhook, index) in webhooksNode" :key="index" class="item" :content="$locale.baseText('nodeWebhooks.clickToCopyWebhookUrls')" placement="left">
|
||||
<div class="webhook-wrapper">
|
||||
<div class="http-field">
|
||||
<div class="http-method">
|
||||
|
@ -83,8 +83,8 @@ export default mixins(
|
|||
this.copyToClipboard(webhookUrl);
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Copied',
|
||||
message: `The webhook URL was successfully copied!`,
|
||||
title: this.$locale.baseText('nodeWebhooks.showMessage.title'),
|
||||
message: this.$locale.baseText('nodeWebhooks.showMessage.message'),
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
@ -95,7 +95,7 @@ export default mixins(
|
|||
try {
|
||||
return this.resolveExpression(webhookData[key] as string) as string;
|
||||
} catch (e) {
|
||||
return '[INVALID EXPRESSION]';
|
||||
return this.$locale.baseText('nodeWebhooks.invalidExpression');
|
||||
}
|
||||
},
|
||||
getWebhookUrl (webhookData: IWebhookDescription): string {
|
||||
|
|
|
@ -35,10 +35,10 @@
|
|||
@focus="setFocus"
|
||||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
:placeholder="isValueExpression?'':parameter.placeholder"
|
||||
:placeholder="isValueExpression ? '' : getPlaceholder()"
|
||||
>
|
||||
<div slot="suffix" class="expand-input-icon-container">
|
||||
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" icon="external-link-alt" class="edit-window-button clickable" title="Open Edit Window" @click="displayEditDialog()" />
|
||||
<font-awesome-icon v-if="!isValueExpression && !isReadOnly" icon="external-link-alt" class="edit-window-button clickable" :title="$locale.baseText('parameterInput.openEditWindow')" @click="displayEditDialog()" />
|
||||
</div>
|
||||
</n8n-input>
|
||||
</div>
|
||||
|
@ -78,7 +78,7 @@
|
|||
:value="displayValue"
|
||||
:title="displayTitle"
|
||||
:disabled="isReadOnly"
|
||||
:placeholder="parameter.placeholder?parameter.placeholder:'Select date and time'"
|
||||
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.selectDateAndTime')"
|
||||
:picker-options="dateTimePickerOptions"
|
||||
@change="valueChanged"
|
||||
@focus="setFocus"
|
||||
|
@ -124,11 +124,13 @@
|
|||
v-for="option in parameterOptions"
|
||||
:value="option.value"
|
||||
:key="option.value"
|
||||
:label="option.name"
|
||||
:label="getOptionsOptionDisplayName(option)"
|
||||
>
|
||||
<div class="list-option">
|
||||
<div class="option-headline">{{ option.name }}</div>
|
||||
<div v-if="option.description" class="option-description" v-html="option.description"></div>
|
||||
<div class="option-headline">
|
||||
{{ getOptionsOptionDisplayName(option) }}
|
||||
</div>
|
||||
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -148,10 +150,10 @@
|
|||
@blur="onBlur"
|
||||
:title="displayTitle"
|
||||
>
|
||||
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="option.name" >
|
||||
<n8n-option v-for="option in parameterOptions" :value="option.value" :key="option.value" :label="getOptionsOptionDisplayName(option)">
|
||||
<div class="list-option">
|
||||
<div class="option-headline">{{ option.name }}</div>
|
||||
<div v-if="option.description" class="option-description" v-html="option.description"></div>
|
||||
<div class="option-headline">{{ getOptionsOptionDisplayName(option) }}</div>
|
||||
<div v-if="option.description" class="option-description" v-html="getOptionsOptionDescription(option)"></div>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
|
@ -169,7 +171,7 @@
|
|||
|
||||
<div class="parameter-issues" v-if="getIssues.length">
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="'Issues:<br /> - ' + getIssues.join('<br /> - ')"></div>
|
||||
<div slot="content" v-html="`${$locale.baseText('parameterInput.issues')}:<br /> - ` + getIssues.join('<br /> - ')"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
@ -177,13 +179,13 @@
|
|||
<div class="parameter-options" v-if="displayOptionsComputed">
|
||||
<el-dropdown trigger="click" @command="optionSelected" size="mini">
|
||||
<span class="el-dropdown-link">
|
||||
<font-awesome-icon icon="cogs" class="reset-icon clickable" title="Parameter Options"/>
|
||||
<font-awesome-icon icon="cogs" class="reset-icon clickable" :title="$locale.baseText('parameterInput.parameterOptions')"/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item command="addExpression" v-if="parameter.noDataExpression !== true && !isValueExpression">Add Expression</el-dropdown-item>
|
||||
<el-dropdown-item command="removeExpression" v-if="parameter.noDataExpression !== true && isValueExpression">Remove Expression</el-dropdown-item>
|
||||
<el-dropdown-item command="refreshOptions" v-if="Boolean(remoteMethod)">Refresh List</el-dropdown-item>
|
||||
<el-dropdown-item command="resetValue" :disabled="isDefault" divided>Reset Value</el-dropdown-item>
|
||||
<el-dropdown-item command="addExpression" v-if="parameter.noDataExpression !== true && !isValueExpression">{{ $locale.baseText('parameterInput.addExpression') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="removeExpression" v-if="parameter.noDataExpression !== true && isValueExpression">{{ $locale.baseText('parameterInput.removeExpression') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="refreshOptions" v-if="Boolean(remoteMethod)">{{ $locale.baseText('parameterInput.refreshList') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="resetValue" :disabled="isDefault" divided>{{ $locale.baseText('parameterInput.resetValue') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
@ -240,6 +242,7 @@ export default mixins(
|
|||
'value',
|
||||
'hideIssues', // boolean
|
||||
'errorHighlight',
|
||||
'isForCredential', // boolean
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
|
@ -255,14 +258,14 @@ export default mixins(
|
|||
dateTimePickerOptions: {
|
||||
shortcuts: [
|
||||
{
|
||||
text: 'Today',
|
||||
text: 'Today', // TODO
|
||||
// tslint:disable-next-line:no-any
|
||||
onClick (picker: any) {
|
||||
picker.$emit('pick', new Date());
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Yesterday',
|
||||
text: 'Yesterday', // TODO
|
||||
// tslint:disable-next-line:no-any
|
||||
onClick (picker: any) {
|
||||
const date = new Date();
|
||||
|
@ -271,7 +274,7 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
{
|
||||
text: 'A week ago',
|
||||
text: 'A week ago', // TODO
|
||||
// tslint:disable-next-line:no-any
|
||||
onClick (picker: any) {
|
||||
const date = new Date();
|
||||
|
@ -325,20 +328,26 @@ export default mixins(
|
|||
return this.$store.getters.activeNode;
|
||||
},
|
||||
displayTitle (): string {
|
||||
let title = `Parameter: "${this.shortPath}"`;
|
||||
if (this.getIssues.length) {
|
||||
title += ` has issues`;
|
||||
if (this.isValueExpression === true) {
|
||||
title += ` and expression`;
|
||||
}
|
||||
title += `!`;
|
||||
} else {
|
||||
if (this.isValueExpression === true) {
|
||||
title += ` has expression`;
|
||||
}
|
||||
const interpolation = { interpolate: { shortPath: this.shortPath } };
|
||||
|
||||
if (this.getIssues.length && this.isValueExpression) {
|
||||
return this.$locale.baseText(
|
||||
'parameterInput.parameterHasIssuesAndExpression',
|
||||
interpolation,
|
||||
);
|
||||
} else if (this.getIssues.length && !this.isValueExpression) {
|
||||
return this.$locale.baseText(
|
||||
'parameterInput.parameterHasIssues',
|
||||
interpolation,
|
||||
);
|
||||
} else if (!this.getIssues.length && this.isValueExpression) {
|
||||
return this.$locale.baseText(
|
||||
'parameterInput.parameterHasExpression',
|
||||
interpolation,
|
||||
);
|
||||
}
|
||||
|
||||
return title;
|
||||
return this.$locale.baseText('parameterInput.parameter', interpolation);
|
||||
},
|
||||
displayValue (): string | number | boolean | null {
|
||||
if (this.remoteParameterOptionsLoading === true) {
|
||||
|
@ -346,7 +355,7 @@ export default mixins(
|
|||
// to user that the data is loading. If not it would
|
||||
// display the user the key instead of the value it
|
||||
// represents
|
||||
return 'Loading options...';
|
||||
return this.$locale.baseText('parameterInput.loadingOptions');
|
||||
}
|
||||
|
||||
let returnValue;
|
||||
|
@ -415,7 +424,7 @@ export default mixins(
|
|||
try {
|
||||
computedValue = this.resolveExpression(this.value) as NodeParameterValue;
|
||||
} catch (error) {
|
||||
computedValue = `[ERROR: ${error.message}]`;
|
||||
computedValue = `[${this.$locale.baseText('parameterInput.error')}}: ${error.message}]`;
|
||||
}
|
||||
|
||||
// Try to convert it into the corret type
|
||||
|
@ -559,6 +568,22 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
getPlaceholder(): string {
|
||||
return this.isForCredential
|
||||
? this.$locale.credText().placeholder(this.parameter)
|
||||
: this.$locale.nodeText().placeholder(this.parameter);
|
||||
},
|
||||
getOptionsOptionDisplayName(option: { value: string; name: string }): string {
|
||||
return this.isForCredential
|
||||
? this.$locale.credText().optionsOptionDisplayName(this.parameter, option)
|
||||
: this.$locale.nodeText().optionsOptionDisplayName(this.parameter, option);
|
||||
},
|
||||
getOptionsOptionDescription(option: { value: string; description: string }): string {
|
||||
return this.isForCredential
|
||||
? this.$locale.credText().optionsOptionDescription(this.parameter, option)
|
||||
: this.$locale.nodeText().optionsOptionDescription(this.parameter, option);
|
||||
},
|
||||
|
||||
async loadRemoteParameterOptions () {
|
||||
if (this.node === null || this.remoteMethod === undefined || this.remoteParameterOptionsLoading) {
|
||||
return;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<n8n-input-label
|
||||
:label="parameter.displayName"
|
||||
:tooltipText="parameter.description"
|
||||
:label="$locale.credText().topParameterDisplayName(parameter)"
|
||||
:tooltipText="$locale.credText().topParameterDescription(parameter)"
|
||||
:required="parameter.required"
|
||||
:showTooltip="focused"
|
||||
>
|
||||
|
@ -13,6 +13,7 @@
|
|||
:displayOptions="true"
|
||||
:documentationUrl="documentationUrl"
|
||||
:errorHighlight="showRequiredErrors"
|
||||
:isForCredential="true"
|
||||
@focus="onFocus"
|
||||
@blur="onBlur"
|
||||
@textInput="valueChanged"
|
||||
|
@ -20,7 +21,7 @@
|
|||
inputSize="large"
|
||||
/>
|
||||
<div class="errors" v-if="showRequiredErrors">
|
||||
This field is required. <a v-if="documentationUrl" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">Open docs</a>
|
||||
{{ $locale.baseText('parameterInputExpanded.thisFieldIsRequired') }} <a v-if="documentationUrl" :href="documentationUrl" target="_blank" @click="onDocumentationUrlClick">{{ $locale.baseText('parameterInputExpanded.openDocs') }}</a>
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
</template>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<n8n-input-label
|
||||
:label="parameter.displayName"
|
||||
:tooltipText="parameter.description"
|
||||
:label="$locale.nodeText().topParameterDisplayName(parameter)"
|
||||
:tooltipText="$locale.nodeText().topParameterDescription(parameter)"
|
||||
:showTooltip="focused"
|
||||
:bold="false"
|
||||
size="small"
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<div v-else-if="parameter.type === 'notice'" class="parameter-item parameter-notice">
|
||||
<n8n-text size="small">
|
||||
<span v-html="parameter.displayName"></span>
|
||||
<span v-html="$locale.nodeText().topParameterDisplayName(parameter)"></span>
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
|
@ -24,17 +24,17 @@
|
|||
v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"
|
||||
class="multi-parameter"
|
||||
>
|
||||
<div class="delete-option clickable" title="Delete" v-if="hideDelete !== true && !isReadOnly">
|
||||
<div class="delete-option clickable" :title="$locale.baseText('parameterInputList.delete')" v-if="hideDelete !== true && !isReadOnly">
|
||||
<font-awesome-icon
|
||||
icon="trash"
|
||||
class="reset-icon clickable"
|
||||
title="Parameter Options"
|
||||
:title="$locale.baseText('parameterInputList.parameterOptions')"
|
||||
@click="deleteOption(parameter.name)"
|
||||
/>
|
||||
</div>
|
||||
<n8n-input-label
|
||||
:label="parameter.displayName"
|
||||
:tooltipText="parameter.description"
|
||||
:label="$locale.nodeText().topParameterDisplayName(parameter)"
|
||||
:tooltipText="$locale.nodeText().topParameterDescription(parameter)"
|
||||
size="small"
|
||||
:underline="true"
|
||||
:labelHoverableOnly="true"
|
||||
|
@ -59,11 +59,11 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="displayNodeParameter(parameter)" class="parameter-item">
|
||||
<div class="delete-option clickable" title="Delete" v-if="hideDelete !== true && !isReadOnly">
|
||||
<div class="delete-option clickable" :title="$locale.baseText('parameterInputList.delete')" v-if="hideDelete !== true && !isReadOnly">
|
||||
<font-awesome-icon
|
||||
icon="trash"
|
||||
class="reset-icon clickable"
|
||||
title="Delete Parameter"
|
||||
:title="$locale.baseText('parameterInputList.deleteParameter')"
|
||||
@click="deleteOption(parameter.name)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<Modal
|
||||
:name="PERSONALIZATION_MODAL_KEY"
|
||||
:title="!submitted? 'Get started' : 'Thanks!'"
|
||||
:subtitle="!submitted? 'These questions help us tailor n8n to you' : ''"
|
||||
:title="!submitted? $locale.baseText('personalizationModal.getStarted') : $locale.baseText('personalizationModal.thanks')"
|
||||
:subtitle="!submitted? $locale.baseText('personalizationModal.theseQuestionsHelpUs') : ''"
|
||||
:centerTitle="true"
|
||||
:showClose="false"
|
||||
:eventBus="modalBus"
|
||||
|
@ -14,109 +14,108 @@
|
|||
<template v-slot:content>
|
||||
<div v-if="submitted" :class="$style.submittedContainer">
|
||||
<img :class="$style.demoImage" :src="baseUrl + 'suggestednodes.png'" />
|
||||
<n8n-text>Look out for things marked with a ✨. They are personalized to make n8n more relevant to you.</n8n-text>
|
||||
<n8n-text>{{ $locale.baseText('personalizationModal.lookOutForThingsMarked') }}</n8n-text>
|
||||
</div>
|
||||
<div :class="$style.container" v-else>
|
||||
<n8n-input-label label="How are your coding skills?">
|
||||
<n8n-select :value="values[CODING_SKILL_KEY]" placeholder="Select..." @change="(value) => values[CODING_SKILL_KEY] = value">
|
||||
<n8n-input-label :label="$locale.baseText('personalizationModal.howAreYourCodingSkills')">
|
||||
<n8n-select :value="values[CODING_SKILL_KEY]" :placeholder="$locale.baseText('personalizationModal.select')" @change="(value) => values[CODING_SKILL_KEY] = value">
|
||||
<n8n-option
|
||||
label="0. Never coded"
|
||||
:label="baseText('personalizationModal.neverCoded')"
|
||||
value="0"
|
||||
/>
|
||||
<n8n-option
|
||||
label="1. I get stuck too quickly to achieve much"
|
||||
:label="baseText('personalizationModal.iGetStuckTooQuicklyToAchieveMuch')"
|
||||
value="1"
|
||||
/>
|
||||
<n8n-option
|
||||
label="2. I can code some useful things, but I spend a lot of time stuck"
|
||||
:label="baseText('personalizationModal.iCanCodeSomeUsefulThingsBut')"
|
||||
value="2"
|
||||
/>
|
||||
<n8n-option
|
||||
label="3. I know enough to be dangerous, but I'm no expert"
|
||||
:label="baseText('personalizationModal.iKnowEnoughToBeDangerousBut')"
|
||||
value="3"
|
||||
/>
|
||||
<n8n-option
|
||||
label="4. I can figure most things out"
|
||||
:label="baseText('personalizationModal.iCanFigureMostThingsOut')"
|
||||
value="4"
|
||||
/>
|
||||
<n8n-option
|
||||
label="5. I can do almost anything I want, easily (pro coder)"
|
||||
:label="baseText('personalizationModal.iCanDoAlmostAnythingIWant')"
|
||||
value="5"
|
||||
/>
|
||||
</n8n-select>
|
||||
</n8n-input-label>
|
||||
|
||||
<n8n-input-label label="Which areas do you mainly work in?">
|
||||
<n8n-select :value="values[WORK_AREA_KEY]" multiple placeholder="Select..." @change="(value) => onMultiInput(WORK_AREA_KEY, value)">
|
||||
<n8n-option :value="FINANCE_WORK_AREA" label="Finance" />
|
||||
<n8n-option :value="HR_WORK_AREA" label="HR" />
|
||||
<n8n-option :value="IT_ENGINEERING_WORK_AREA" label="IT / Engineering" />
|
||||
<n8n-option :value="LEGAL_WORK_AREA" label="Legal" />
|
||||
<n8n-option :value="MARKETING_WORK_AREA" label="Marketing" />
|
||||
<n8n-option :value="OPS_WORK_AREA" label="Operations" />
|
||||
<n8n-option :value="PRODUCT_WORK_AREA" label="Product" />
|
||||
<n8n-option :value="SALES_BUSINESSDEV_WORK_AREA" label="Sales / Bizdev" />
|
||||
<n8n-option :value="SECURITY_WORK_AREA" label="Security" />
|
||||
<n8n-option :value="SUPPORT_WORK_AREA" label="Support" />
|
||||
<n8n-option :value="EXECUTIVE_WORK_AREA" label="Executive team" />
|
||||
<n8n-option :value="OTHER_WORK_AREA_OPTION" label="Other (please specify)" />
|
||||
<n8n-option :value="NOT_APPLICABLE_WORK_AREA" label="I'm not using n8n for work" />
|
||||
<n8n-input-label :label="$locale.baseText('personalizationModal.whichOfTheseAreasDoYouMainlyWorkIn')">
|
||||
<n8n-select :value="values[WORK_AREA_KEY]" multiple :placeholder="$locale.baseText('personalizationModal.select')" @change="(value) => onMultiInput(WORK_AREA_KEY, value)">
|
||||
<n8n-option :value="FINANCE_WORK_AREA" :label="$locale.baseText('personalizationModal.finance')" />
|
||||
<n8n-option :value="HR_WORK_AREA" :label="$locale.baseText('personalizationModal.hr')" />
|
||||
<n8n-option :value="IT_ENGINEERING_WORK_AREA" :label="$locale.baseText('personalizationModal.itEngineering')" />
|
||||
<n8n-option :value="LEGAL_WORK_AREA" :label="$locale.baseText('personalizationModal.legal')" />
|
||||
<n8n-option :value="MARKETING_WORK_AREA" :label="$locale.baseText('personalizationModal.marketing')" />
|
||||
<n8n-option :value="OPS_WORK_AREA" :label="$locale.baseText('personalizationModal.operations')" />
|
||||
<n8n-option :value="PRODUCT_WORK_AREA" :label="$locale.baseText('personalizationModal.product')" />
|
||||
<n8n-option :value="SALES_BUSINESSDEV_WORK_AREA" :label="$locale.baseText('personalizationModal.salesBizDev')" />
|
||||
<n8n-option :value="SECURITY_WORK_AREA" :label="$locale.baseText('personalizationModal.security')" />
|
||||
<n8n-option :value="SUPPORT_WORK_AREA" :label="$locale.baseText('personalizationModal.support')" />
|
||||
<n8n-option :value="EXECUTIVE_WORK_AREA" :label="$locale.baseText('personalizationModal.executiveTeam')" />
|
||||
<n8n-option :value="OTHER_WORK_AREA_OPTION" :label="$locale.baseText('personalizationModal.otherPleaseSpecify')" />
|
||||
<n8n-option :value="NOT_APPLICABLE_WORK_AREA" :label="$locale.baseText('personalizationModal.imNotUsingN8nForWork')" />
|
||||
</n8n-select>
|
||||
</n8n-input-label>
|
||||
<n8n-input
|
||||
v-if="otherWorkAreaFieldVisible"
|
||||
:value="values[OTHER_WORK_AREA_KEY]"
|
||||
placeholder="Specify your work area"
|
||||
:placeholder="$locale.baseText('personalizationModal.specifyYourWorkArea')"
|
||||
@input="(value) => values[OTHER_WORK_AREA_KEY] = value"
|
||||
/>
|
||||
|
||||
<section v-if="showAllIndustryQuestions">
|
||||
<n8n-input-label label="Which industries is your company in?">
|
||||
<n8n-select :value="values[COMPANY_INDUSTRY_KEY]" multiple placeholder="Select..." @change="(value) => onMultiInput(COMPANY_INDUSTRY_KEY, value)">
|
||||
<n8n-option :value="E_COMMERCE_INDUSTRY" label="eCommerce" />
|
||||
<n8n-option :value="AUTOMATION_CONSULTING_INDUSTRY" label="Automation consulting" />
|
||||
<n8n-option :value="SYSTEM_INTEGRATION_INDUSTRY" label="Systems integration" />
|
||||
<n8n-option :value="GOVERNMENT_INDUSTRY" label="Government" />
|
||||
<n8n-option :value="LEGAL_INDUSTRY" label="Legal" />
|
||||
<n8n-option :value="HEALTHCARE_INDUSTRY" label="Healthcare" />
|
||||
<n8n-option :value="FINANCE_INDUSTRY" label="Finance" />
|
||||
<n8n-option :value="SECURITY_INDUSTRY" label="Security" />
|
||||
<n8n-option :value="SAAS_INDUSTRY" label="SaaS" />
|
||||
<n8n-option :value="OTHER_INDUSTRY_OPTION" label="Other (please specify)" />
|
||||
<n8n-input-label :label="$locale.baseText('personalizationModal.whichIndustriesIsYourCompanyIn')">
|
||||
<n8n-select :value="values[COMPANY_INDUSTRY_KEY]" multiple :placeholder="$locale.baseText('personalizationModal.select')" @change="(value) => onMultiInput(COMPANY_INDUSTRY_KEY, value)">
|
||||
<n8n-option :value="E_COMMERCE_INDUSTRY" :label="$locale.baseText('personalizationModal.eCommerce')" />
|
||||
<n8n-option :value="AUTOMATION_CONSULTING_INDUSTRY" :label="$locale.baseText('personalizationModal.automationConsulting')" />
|
||||
<n8n-option :value="SYSTEM_INTEGRATION_INDUSTRY" :label="$locale.baseText('personalizationModal.systemsIntegration')" />
|
||||
<n8n-option :value="GOVERNMENT_INDUSTRY" :label="$locale.baseText('personalizationModal.government')" />
|
||||
<n8n-option :value="LEGAL_INDUSTRY" :label="$locale.baseText('personalizationModal.legal')" />
|
||||
<n8n-option :value="HEALTHCARE_INDUSTRY" :label="$locale.baseText('personalizationModal.healthcare')" />
|
||||
<n8n-option :value="FINANCE_INDUSTRY" :label="$locale.baseText('personalizationModal.finance')" />
|
||||
<n8n-option :value="SECURITY_INDUSTRY" :label="$locale.baseText('personalizationModal.security')" />
|
||||
<n8n-option :value="SAAS_INDUSTRY" :label="$locale.baseText('personalizationModal.saas')" />
|
||||
<n8n-option :value="OTHER_INDUSTRY_OPTION" :label="$locale.baseText('personalizationModal.otherPleaseSpecify')" />
|
||||
</n8n-select>
|
||||
</n8n-input-label>
|
||||
<n8n-input
|
||||
v-if="otherCompanyIndustryFieldVisible"
|
||||
:value="values[OTHER_COMPANY_INDUSTRY_KEY]"
|
||||
placeholder="Specify your company's industry"
|
||||
:placeholder="$locale.baseText('personalizationModal.specifyYourCompanysIndustry')"
|
||||
@input="(value) => values[OTHER_COMPANY_INDUSTRY_KEY] = value"
|
||||
/>
|
||||
|
||||
|
||||
<n8n-input-label label="How big is your company?">
|
||||
<n8n-input-label :label="$locale.baseText('personalizationModal.howBigIsYourCompany')">
|
||||
<n8n-select :value="values[COMPANY_SIZE_KEY]" placeholder="Select..." @change="(value) => values[COMPANY_SIZE_KEY] = value">
|
||||
<n8n-option
|
||||
label="Less than 20 people"
|
||||
:label="$locale.baseText('personalizationModal.lessThan20People')"
|
||||
:value="COMPANY_SIZE_20_OR_LESS"
|
||||
/>
|
||||
<n8n-option
|
||||
label="20-99 people"
|
||||
:label="`20-99 ${$locale.baseText('personalizationModal.people')}`"
|
||||
:value="COMPANY_SIZE_20_99"
|
||||
/>
|
||||
<n8n-option
|
||||
label="100-499 people"
|
||||
:label="`100-499 ${$locale.baseText('personalizationModal.people')}`"
|
||||
:value="COMPANY_SIZE_100_499"
|
||||
/>
|
||||
<n8n-option
|
||||
label="500-999 people"
|
||||
:label="`500-999 ${$locale.baseText('personalizationModal.people')}`"
|
||||
:value="COMPANY_SIZE_500_999"
|
||||
/>
|
||||
<n8n-option
|
||||
label="1000+ people"
|
||||
:label="`1000+ ${$locale.baseText('personalizationModal.people')}`"
|
||||
:value="COMPANY_SIZE_1000_OR_MORE"
|
||||
/>
|
||||
<n8n-option
|
||||
label="I'm not using n8n for work"
|
||||
:label="$locale.baseText('personalizationModal.imNotUsingN8nForWork')"
|
||||
:value="COMPANY_SIZE_PERSONAL_USE"
|
||||
/>
|
||||
</n8n-select>
|
||||
|
@ -127,8 +126,8 @@
|
|||
</template>
|
||||
<template v-slot:footer>
|
||||
<div>
|
||||
<n8n-button v-if="submitted" @click="closeDialog" label="Get started" float="right" />
|
||||
<n8n-button v-else @click="save" :loading="isSaving" label="Continue" float="right" />
|
||||
<n8n-button v-if="submitted" @click="closeDialog" :label="$locale.baseText('personalizationModal.getStarted')" float="right" />
|
||||
<n8n-button v-else @click="save" :loading="isSaving" :label="$locale.baseText('personalizationModal.continue')" float="right" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
<div class="push-connection-lost primary-color" v-if="!pushConnectionActive">
|
||||
<n8n-tooltip placement="bottom-end" >
|
||||
<div slot="content">
|
||||
Cannot connect to server.<br />
|
||||
It is either down or you have a connection issue. <br />
|
||||
It should reconnect automatically once the issue is resolved.
|
||||
{{ $locale.baseText('pushConnectionTracker.cannotConnectToServer') }}
|
||||
</div>
|
||||
<span>
|
||||
<font-awesome-icon icon="exclamation-triangle" /> Connection lost
|
||||
<font-awesome-icon icon="exclamation-triangle" /> {{ $locale.baseText('pushConnectionTracker.connectionLost') }}
|
||||
</span>
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
class="execute-node-button"
|
||||
>
|
||||
<n8n-button
|
||||
:title="`Executes this ${node.name} node after executing any previous nodes that have not yet returned data`"
|
||||
:title="$locale.baseText('runData.executesThisNodeAfterExecuting', { interpolate: { nodeName: node.name } })"
|
||||
:loading="workflowRunning"
|
||||
icon="play-circle"
|
||||
label="Execute Node"
|
||||
:label="$locale.baseText('runData.executeNode')"
|
||||
@click.stop="runWorkflow(node.name, 'RunData.ExecuteNodeButton')"
|
||||
/>
|
||||
</div>
|
||||
|
@ -18,10 +18,10 @@
|
|||
<div class="header">
|
||||
<div class="title-text">
|
||||
<n8n-text :bold="true" v-if="dataCount < maxDisplayItems">
|
||||
Items: {{ dataCount }}
|
||||
{{ $locale.baseText('runData.items') }}: {{ dataCount }}
|
||||
</n8n-text>
|
||||
<div v-else class="title-text">
|
||||
<n8n-text :bold="true">Items:</n8n-text>
|
||||
<n8n-text :bold="true">{{ $locale.baseText('runData.items') }}:</n8n-text>
|
||||
<span class="opts">
|
||||
<n8n-select size="mini" v-model="maxDisplayItems" @click.stop>
|
||||
<n8n-option v-for="option in maxDisplayItemsOptions" :label="option" :value="option" :key="option" />
|
||||
|
@ -34,13 +34,13 @@
|
|||
placement="right"
|
||||
>
|
||||
<div slot="content">
|
||||
<n8n-text :bold="true" size="small">Start Time:</n8n-text> {{runMetadata.startTime}}<br/>
|
||||
<n8n-text :bold="true" size="small">Execution Time:</n8n-text> {{runMetadata.executionTime}} ms
|
||||
<n8n-text :bold="true" size="small">{{ $locale.baseText('runData.startTime') + ':' }}</n8n-text> {{runMetadata.startTime}}<br/>
|
||||
<n8n-text :bold="true" size="small">{{ $locale.baseText('runData.executionTime') + ':' }}</n8n-text> {{runMetadata.executionTime}} {{ $locale.baseText('runData.ms') }}
|
||||
</div>
|
||||
<font-awesome-icon icon="info-circle" class="primary-color" />
|
||||
</n8n-tooltip>
|
||||
<n8n-text :bold="true" v-if="maxOutputIndex > 0">
|
||||
| Output:
|
||||
| {{ $locale.baseText('runData.output') }}:
|
||||
</n8n-text>
|
||||
<span class="opts" v-if="maxOutputIndex > 0" >
|
||||
<n8n-select size="mini" v-model="outputIndex" @click.stop>
|
||||
|
@ -50,7 +50,7 @@
|
|||
</span>
|
||||
|
||||
<n8n-text :bold="true" v-if="maxRunIndex > 0">
|
||||
| Data of Execution:
|
||||
| {{ $locale.baseText('runData.dataOfExecution') }}:
|
||||
</n8n-text>
|
||||
<span class="opts">
|
||||
<n8n-select v-if="maxRunIndex > 0" size="mini" v-model="runIndex" @click.stop>
|
||||
|
@ -62,20 +62,26 @@
|
|||
</div>
|
||||
<div v-if="hasNodeRun && !hasRunError" class="title-data-display-selector" @click.stop>
|
||||
<el-radio-group v-model="displayMode" size="mini">
|
||||
<el-radio-button label="JSON" :disabled="showData === false"></el-radio-button>
|
||||
<el-radio-button label="Table"></el-radio-button>
|
||||
<el-radio-button label="Binary" v-if="binaryData.length !== 0"></el-radio-button>
|
||||
<el-radio-button :label="$locale.baseText('runData.json')" :disabled="showData === false"></el-radio-button>
|
||||
<el-radio-button :label="$locale.baseText('runData.table')"></el-radio-button>
|
||||
<el-radio-button :label="$locale.baseText('runData.binary')" v-if="binaryData.length !== 0"></el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div v-if="hasNodeRun && !hasRunError && displayMode === 'JSON' && state.path !== deselectedPlaceholder" class="select-button">
|
||||
<el-dropdown trigger="click" @command="handleCopyClick">
|
||||
<span class="el-dropdown-link">
|
||||
<n8n-icon-button title="Copy to Clipboard" icon="copy" />
|
||||
<n8n-icon-button :title="$locale.baseText('runData.copyToClipboard')" icon="copy" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'itemPath'}">Copy Item Path</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'parameterPath'}">Copy Parameter Path</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'value'}">Copy Value</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'itemPath'}">
|
||||
{{ $locale.baseText('runData.copyItemPath') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'parameterPath'}">
|
||||
{{ $locale.baseText('runData.copyParameterPath') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'value'}">
|
||||
{{ $locale.baseText('runData.copyValue') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
@ -88,29 +94,33 @@
|
|||
<span v-else>
|
||||
<div v-if="showData === false" class="too-much-data">
|
||||
<h3>
|
||||
Node returned a large amount of data
|
||||
{{ $locale.baseText('runData.nodeReturnedALargeAmountOfData') }}
|
||||
</h3>
|
||||
|
||||
<div class="text">
|
||||
The node contains {{parseInt(dataSize/1024).toLocaleString()}} KB of data.<br />
|
||||
Displaying it could cause problems!<br />
|
||||
<br />
|
||||
If you do decide to display it, avoid the JSON view!
|
||||
{{ $locale.baseText(
|
||||
'runData.theNodeContains',
|
||||
{
|
||||
interpolate: {
|
||||
numberOfKb: parseInt(dataSize/1024).toLocaleString()
|
||||
}
|
||||
}
|
||||
)}}
|
||||
</div>
|
||||
|
||||
<n8n-button
|
||||
icon="eye"
|
||||
label="Display Data Anyway"
|
||||
:label="$locale.baseText('runData.displayDataAnyway')"
|
||||
@click="displayMode = 'Table';showData = true;"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="['JSON', 'Table'].includes(displayMode)">
|
||||
<div v-if="jsonData.length === 0" class="no-data">
|
||||
No text data found
|
||||
{{ $locale.baseText('runData.noTextDataFound') }}
|
||||
</div>
|
||||
<div v-else-if="displayMode === 'Table'">
|
||||
<div v-if="tableData !== null && tableData.columns.length === 0" class="no-data">
|
||||
Entries exist but they do not contain any JSON data.
|
||||
{{ $locale.baseText('runData.entriesExistButThey') }}
|
||||
</div>
|
||||
<table v-else-if="tableData !== null">
|
||||
<tr>
|
||||
|
@ -138,7 +148,7 @@
|
|||
</div>
|
||||
<div v-else-if="displayMode === 'Binary'">
|
||||
<div v-if="binaryData.length === 0" class="no-data">
|
||||
No binary data found
|
||||
{{ $locale.baseText('runData.noBinaryDataFound') }}
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
|
@ -156,24 +166,24 @@
|
|||
{{key}}
|
||||
</div>
|
||||
<div v-if="binaryData.fileName">
|
||||
<div class="label">File Name: </div>
|
||||
<div class="label">{{ $locale.baseText('runData.fileName') }}: </div>
|
||||
<div class="value">{{binaryData.fileName}}</div>
|
||||
</div>
|
||||
<div v-if="binaryData.directory">
|
||||
<div class="label">Directory: </div>
|
||||
<div class="label">{{ $locale.baseText('runData.directory') }}: </div>
|
||||
<div class="value">{{binaryData.directory}}</div>
|
||||
</div>
|
||||
<div v-if="binaryData.fileExtension">
|
||||
<div class="label">File Extension:</div>
|
||||
<div class="label">{{ $locale.baseText('runData.fileExtension') }}:</div>
|
||||
<div class="value">{{binaryData.fileExtension}}</div>
|
||||
</div>
|
||||
<div v-if="binaryData.mimeType">
|
||||
<div class="label">Mime Type: </div>
|
||||
<div class="label">{{ $locale.baseText('runData.mimeType') }}: </div>
|
||||
<div class="value">{{binaryData.mimeType}}</div>
|
||||
</div>
|
||||
|
||||
<div class="binary-data-show-data-button-wrapper">
|
||||
<n8n-button size="small" label="Show Binary Data" class="binary-data-show-data-button" @click="displayBinaryData(index, key)" />
|
||||
<n8n-button size="small" :label="$locale.baseText('runData.showBinaryData')" class="binary-data-show-data-button" @click="displayBinaryData(index, key)" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@ -186,9 +196,9 @@
|
|||
</span>
|
||||
<div v-else class="message">
|
||||
<div>
|
||||
<n8n-text :bold="true">No data</n8n-text ><br />
|
||||
<n8n-text :bold="true">{{ $locale.baseText('runData.noData') }}</n8n-text ><br />
|
||||
<br />
|
||||
Data returned by this node will display here<br />
|
||||
{{ $locale.baseText('runData.dataReturnedByThisNodeWillDisplayHere') }}<br />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -222,7 +232,7 @@ import {
|
|||
} from '@/constants';
|
||||
|
||||
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
||||
import NodeErrorView from '@/components/Error/NodeViewError.vue';
|
||||
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
|
||||
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { externalHooks } from "@/components/mixins/externalHooks";
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<span :class="$style.container">
|
||||
<span :class="$style.saved" v-if="saved">{{ savedLabel }}</span>
|
||||
<span :class="$style.saved" v-if="saved">{{ $locale.baseText('saveButton.saved') }}</span>
|
||||
<n8n-button
|
||||
v-else
|
||||
:label="isSaving ? savingLabel : saveLabel"
|
||||
:label="saveButtonLabel"
|
||||
:loading="isSaving"
|
||||
:disabled="disabled"
|
||||
@click="$emit('click')"
|
||||
|
@ -28,15 +28,19 @@ export default Vue.extend({
|
|||
},
|
||||
saveLabel: {
|
||||
type: String,
|
||||
default: 'Save',
|
||||
},
|
||||
savingLabel: {
|
||||
type: String,
|
||||
default: 'Saving',
|
||||
},
|
||||
savedLabel: {
|
||||
type: String,
|
||||
default: 'Saved',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
saveButtonLabel() {
|
||||
return this.isSaving
|
||||
? this.$locale.baseText('saveButton.saving')
|
||||
: this.$locale.baseText('saveButton.save');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -24,12 +24,14 @@
|
|||
ref="create"
|
||||
>
|
||||
<font-awesome-icon icon="plus-circle" />
|
||||
<span>Create tag "{{ filter }}"</span>
|
||||
<span>
|
||||
{{ $locale.baseText('tagsDropdown.createTag', { interpolate: { filter } }) }}
|
||||
</span>
|
||||
</n8n-option>
|
||||
<n8n-option v-else-if="options.length === 0" value="message" disabled>
|
||||
<span v-if="createEnabled">Type to create a tag</span>
|
||||
<span v-else-if="allTags.length > 0">No matching tags exist</span>
|
||||
<span v-else>No tags exist</span>
|
||||
<span v-if="createEnabled">{{ $locale.baseText('tagsDropdown.typeToCreateATag') }}</span>
|
||||
<span v-else-if="allTags.length > 0">{{ $locale.baseText('tagsDropdown.noMatchingTagsExist') }}</span>
|
||||
<span v-else>{{ $locale.baseText('tagsDropdown.noTagsExist') }}</span>
|
||||
</n8n-option>
|
||||
|
||||
<!-- key is id+index for keyboard navigation to work well with filter -->
|
||||
|
@ -44,7 +46,7 @@
|
|||
|
||||
<n8n-option :key="MANAGE_KEY" :value="MANAGE_KEY" class="ops manage-tags">
|
||||
<font-awesome-icon icon="cog" />
|
||||
<span>Manage tags</span>
|
||||
<span>{{ $locale.baseText('tagsDropdown.manageTags') }}</span>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</div>
|
||||
|
@ -139,8 +141,11 @@ export default mixins(showMessage).extend({
|
|||
} catch (error) {
|
||||
this.$showError(
|
||||
error,
|
||||
"New tag was not created",
|
||||
`A problem occurred when trying to create the "${name}" tag`,
|
||||
this.$locale.baseText('tagsDropdown.showError.title'),
|
||||
this.$locale.baseText(
|
||||
'tagsDropdown.showError.message',
|
||||
{ interpolate: { name } },
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,10 +3,9 @@
|
|||
<el-col class="notags" :span="16">
|
||||
<div class="icon">🗄️</div>
|
||||
<div>
|
||||
<div class="headline">Ready to organize your workflows?</div>
|
||||
<div class="headline">{{ $locale.baseText('noTagsView.readyToOrganizeYourWorkflows') }}</div>
|
||||
<div class="description">
|
||||
With workflow tags, you're free to create the perfect tagging system for
|
||||
your flows
|
||||
{{ $locale.baseText('noTagsView.withWorkflowTagsYouReFree') }}
|
||||
</div>
|
||||
</div>
|
||||
<n8n-button label="Create a tag" size="large" @click="$emit('enableCreate')" />
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<Modal
|
||||
title="Manage tags"
|
||||
:title="$locale.baseText('tagsManager.manageTags')"
|
||||
:name="TAGS_MANAGER_MODAL_KEY"
|
||||
:eventBus="modalBus"
|
||||
@enter="onEnter"
|
||||
|
@ -25,7 +25,7 @@
|
|||
</el-row>
|
||||
</template>
|
||||
<template v-slot:footer="{ close }">
|
||||
<n8n-button label="Done" @click="close" float="right" />
|
||||
<n8n-button :label="$locale.baseText('tagsManager.done')" @click="close" float="right" />
|
||||
</template>
|
||||
</Modal>
|
||||
</template>
|
||||
|
@ -86,7 +86,9 @@ export default mixins(showMessage).extend({
|
|||
async onCreate(name: string, cb: (tag: ITag | null, error?: Error) => void) {
|
||||
try {
|
||||
if (!name) {
|
||||
throw new Error("Tag name cannot be empty");
|
||||
throw new Error(
|
||||
this.$locale.baseText('tagsManager.tagNameCannotBeEmpty'),
|
||||
);
|
||||
}
|
||||
|
||||
const newTag = await this.$store.dispatch("tags/create", name);
|
||||
|
@ -96,8 +98,11 @@ export default mixins(showMessage).extend({
|
|||
const escapedName = escape(name);
|
||||
this.$showError(
|
||||
error,
|
||||
"New tag was not created",
|
||||
`A problem occurred when trying to create the "${escapedName}" tag`,
|
||||
this.$locale.baseText('tagsManager.showError.onCreate.title'),
|
||||
this.$locale.baseText(
|
||||
'tagsManager.showError.onCreate.message',
|
||||
{ interpolate: { escapedName } },
|
||||
) + ':',
|
||||
);
|
||||
cb(null, error);
|
||||
}
|
||||
|
@ -109,7 +114,9 @@ export default mixins(showMessage).extend({
|
|||
|
||||
try {
|
||||
if (!name) {
|
||||
throw new Error("Tag name cannot be empty");
|
||||
throw new Error(
|
||||
this.$locale.baseText('tagsManager.tagNameCannotBeEmpty'),
|
||||
);
|
||||
}
|
||||
|
||||
if (name === oldName) {
|
||||
|
@ -124,16 +131,22 @@ export default mixins(showMessage).extend({
|
|||
const escapedOldName = escape(oldName);
|
||||
|
||||
this.$showMessage({
|
||||
title: "Tag was updated",
|
||||
message: `The "${escapedOldName}" tag was successfully updated to "${escapedName}"`,
|
||||
title: this.$locale.baseText('tagsManager.showMessage.onUpdate.title'),
|
||||
message: this.$locale.baseText(
|
||||
'tagsManager.showMessage.onUpdate.message',
|
||||
{ interpolate: { escapedName, escapedOldName } },
|
||||
),
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
const escapedName = escape(oldName);
|
||||
this.$showError(
|
||||
error,
|
||||
"Tag was not updated",
|
||||
`A problem occurred when trying to update the "${escapedName}" tag`,
|
||||
this.$locale.baseText('tagsManager.showError.onUpdate.title'),
|
||||
this.$locale.baseText(
|
||||
'tagsManager.showError.onUpdate.message',
|
||||
{ interpolate: { escapedName } },
|
||||
) + ':',
|
||||
);
|
||||
cb(false, error);
|
||||
}
|
||||
|
@ -146,7 +159,9 @@ export default mixins(showMessage).extend({
|
|||
try {
|
||||
const deleted = await this.$store.dispatch("tags/delete", id);
|
||||
if (!deleted) {
|
||||
throw new Error('Could not delete tag');
|
||||
throw new Error(
|
||||
this.$locale.baseText('tagsManager.couldNotDeleteTag'),
|
||||
);
|
||||
}
|
||||
|
||||
this.$data.tagIds = this.$data.tagIds.filter((tagId: string) => tagId !== id);
|
||||
|
@ -155,16 +170,22 @@ export default mixins(showMessage).extend({
|
|||
|
||||
const escapedName = escape(name);
|
||||
this.$showMessage({
|
||||
title: "Tag was deleted",
|
||||
message: `The "${escapedName}" tag was successfully deleted from your tag collection`,
|
||||
title: this.$locale.baseText('tagsManager.showMessage.onDelete.title'),
|
||||
message: this.$locale.baseText(
|
||||
'tagsManager.showMessage.onDelete.message',
|
||||
{ interpolate: { escapedName } },
|
||||
),
|
||||
type: "success",
|
||||
});
|
||||
} catch (error) {
|
||||
const escapedName = escape(name);
|
||||
this.$showError(
|
||||
error,
|
||||
"Tag was not deleted",
|
||||
`A problem occurred when trying to delete the "${escapedName}" tag`,
|
||||
this.$locale.baseText('tagsManager.showError.onDelete.title'),
|
||||
this.$locale.baseText(
|
||||
'tagsManager.showError.onDelete.message',
|
||||
{ interpolate: { escapedName } },
|
||||
) + ':',
|
||||
);
|
||||
cb(false, error);
|
||||
}
|
||||
|
|
|
@ -3,13 +3,13 @@
|
|||
stripe
|
||||
max-height="450"
|
||||
ref="table"
|
||||
empty-text="No matching tags exist"
|
||||
:empty-text="$locale.baseText('tagsTable.noMatchingTagsExist')"
|
||||
:data="rows"
|
||||
:span-method="getSpan"
|
||||
:row-class-name="getRowClasses"
|
||||
v-loading="isLoading"
|
||||
>
|
||||
<el-table-column label="Name">
|
||||
<el-table-column :label="$locale.baseText('tagsTable.name')">
|
||||
<template slot-scope="scope">
|
||||
<div class="name" :key="scope.row.id" @keydown.stop>
|
||||
<transition name="fade" mode="out-in">
|
||||
|
@ -21,7 +21,7 @@
|
|||
ref="nameInput"
|
||||
></n8n-input>
|
||||
<span v-else-if="scope.row.delete">
|
||||
<span>Are you sure you want to delete this tag?</span>
|
||||
<span>{{ $locale.baseText('tagsTable.areYouSureYouWantToDeleteThisTag') }}</span>
|
||||
<input ref="deleteHiddenInput" class="hidden" />
|
||||
</span>
|
||||
<span v-else :class="{ disabled: scope.row.disable }">
|
||||
|
@ -31,7 +31,7 @@
|
|||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="Usage" width="150">
|
||||
<el-table-column :label="$locale.baseText('tagsTable.usage')" width="150">
|
||||
<template slot-scope="scope">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div v-if="!scope.row.create && !scope.row.delete" :class="{ disabled: scope.row.disable }">
|
||||
|
@ -44,20 +44,20 @@
|
|||
<template slot-scope="scope">
|
||||
<transition name="fade" mode="out-in">
|
||||
<div class="ops" v-if="scope.row.create">
|
||||
<n8n-button label="Cancel" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button label="Create tag" @click.stop="apply" :loading="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.createTag')" @click.stop="apply" :loading="isSaving" />
|
||||
</div>
|
||||
<div class="ops" v-else-if="scope.row.update">
|
||||
<n8n-button label="Cancel" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button label="Save changes" @click.stop="apply" :loading="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.saveChanges')" @click.stop="apply" :loading="isSaving" />
|
||||
</div>
|
||||
<div class="ops" v-else-if="scope.row.delete">
|
||||
<n8n-button label="Cancel" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button label="Delete tag" @click.stop="apply" :loading="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.cancel')" @click.stop="cancel" type="outline" :disabled="isSaving" />
|
||||
<n8n-button :label="$locale.baseText('tagsTable.deleteTag')" @click.stop="apply" :loading="isSaving" />
|
||||
</div>
|
||||
<div class="ops main" v-else-if="!scope.row.disable">
|
||||
<n8n-icon-button title="Edit Tag" @click.stop="enableUpdate(scope.row)" icon="pen" />
|
||||
<n8n-icon-button title="Delete Tag" @click.stop="enableDelete(scope.row)" icon="trash" />
|
||||
<n8n-icon-button :title="$locale.baseText('tagsTable.editTag')" @click.stop="enableUpdate(scope.row)" icon="pen" />
|
||||
<n8n-icon-button :title="$locale.baseText('tagsTable.deleteTag')" @click.stop="enableDelete(scope.row)" icon="trash" />
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<el-row class="tags-header">
|
||||
<el-col :span="10">
|
||||
<n8n-input
|
||||
placeholder="Search tags"
|
||||
:placeholder="$locale.baseText('tagsTableHeader.searchTags')"
|
||||
:value="search"
|
||||
@input="onSearchChange"
|
||||
:disabled="disabled"
|
||||
|
@ -13,7 +13,7 @@
|
|||
</n8n-input>
|
||||
</el-col>
|
||||
<el-col :span="14">
|
||||
<n8n-button @click="onAddNew" :disabled="disabled" icon="plus" label="Add new" size="large" float="right" />
|
||||
<n8n-button @click="onAddNew" :disabled="disabled" icon="plus" :label="$locale.baseText('tagsTableHeader.addNew')" size="large" float="right" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
|
@ -21,6 +21,7 @@
|
|||
<script lang="ts">
|
||||
import { MAX_TAG_NAME_LENGTH } from "@/constants";
|
||||
import Vue from "vue";
|
||||
|
||||
export default Vue.extend({
|
||||
props: {
|
||||
disabled: {
|
||||
|
|
|
@ -31,9 +31,9 @@ import Vue from "vue";
|
|||
import { ITag, ITagRow } from "@/Interface";
|
||||
import TagsTableHeader from "@/components/TagsManager/TagsView/TagsTableHeader.vue";
|
||||
import TagsTable from "@/components/TagsManager/TagsView/TagsTable.vue";
|
||||
import mixins from "vue-typed-mixins";
|
||||
|
||||
const matches = (name: string, filter: string) => name.toLowerCase().trim().includes(filter.toLowerCase().trim());
|
||||
const getUsage = (count: number | undefined) => count && count > 0 ? `${count} workflow${count > 1 ? "s" : ""}` : 'Not being used';
|
||||
|
||||
export default Vue.extend({
|
||||
components: { TagsTableHeader, TagsTable },
|
||||
|
@ -55,6 +55,18 @@ export default Vue.extend({
|
|||
return (this.$props.tags || []).length === 0 || this.$data.createEnabled;
|
||||
},
|
||||
rows(): ITagRow[] {
|
||||
const getUsage = (count: number | undefined) => count && count > 0
|
||||
? this.$locale.baseText(
|
||||
count > 1 ?
|
||||
'tagsView.inUse.plural' : 'tagsView.inUse.singular',
|
||||
{
|
||||
interpolate: {
|
||||
count: count.toString(),
|
||||
},
|
||||
},
|
||||
)
|
||||
: this.$locale.baseText('tagsView.notBeingUsed');
|
||||
|
||||
const disabled = this.isCreateEnabled || this.$data.updateId || this.$data.deleteId;
|
||||
const tagRows = (this.$props.tags || [])
|
||||
.filter((tag: ITag) => this.stickyIds.has(tag.id) || matches(tag.name, this.$data.search))
|
||||
|
@ -102,7 +114,7 @@ export default Vue.extend({
|
|||
this.stickyIds.add(this.updateId);
|
||||
this.disableUpdate();
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
this.$emit("update", this.updateId, name, onUpdate);
|
||||
},
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div v-if="dialogVisible">
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`Edit ${parameter.displayName}`" :before-close="closeDialog">
|
||||
<el-dialog :visible="dialogVisible" append-to-body width="80%" :title="`${$locale.baseText('textEdit.edit')} ${$locale.nodeText().topParameterDisplayName(parameter)}`" :before-close="closeDialog">
|
||||
|
||||
<div class="ignore-key-press">
|
||||
<n8n-input-label :label="parameter.displayName">
|
||||
<n8n-input-label :label="$locale.nodeText().topParameterDisplayName(parameter)">
|
||||
<div @keydown.stop @keydown.esc="closeDialog()">
|
||||
<n8n-input v-model="tempValue" type="textarea" ref="inputField" :value="value" :placeholder="parameter.placeholder" @change="valueChanged" @keydown.stop="noOp" :rows="15" />
|
||||
<n8n-input v-model="tempValue" type="textarea" ref="inputField" :value="value" :placeholder="$locale.nodeText().placeholder(parameter)" @change="valueChanged" @keydown.stop="noOp" :rows="15" />
|
||||
</div>
|
||||
</n8n-input-label>
|
||||
</div>
|
||||
|
@ -18,7 +18,6 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
|
||||
name: 'TextEdit',
|
||||
props: [
|
||||
'dialogVisible',
|
||||
|
|
|
@ -1,39 +1,17 @@
|
|||
<template functional>
|
||||
<span :title="$options.methods.convertToHumanReadableDate($props)">
|
||||
{{$options.methods.format(props)}}
|
||||
<template>
|
||||
<span :title="convertDate">
|
||||
{{ format }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { format, LocaleFunc, register } from 'timeago.js';
|
||||
import { convertToHumanReadableDate } from './helpers';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
const localeFunc = (num: number, index: number, totalSec: number): [string, string] => {
|
||||
// number: the timeago / timein number;
|
||||
// index: the index of array below;
|
||||
// totalSec: total seconds between date to be formatted and today's date;
|
||||
return [
|
||||
['Just now', 'Right now'],
|
||||
['Just now', 'Right now'], // ['%s seconds ago', 'in %s seconds'],
|
||||
['1 minute ago', 'in 1 minute'],
|
||||
['%s minutes ago', 'in %s minutes'],
|
||||
['1 hour ago', 'in 1 hour'],
|
||||
['%s hours ago', 'in %s hours'],
|
||||
['1 day ago', 'in 1 day'],
|
||||
['%s days ago', 'in %s days'],
|
||||
['1 week ago', 'in 1 week'],
|
||||
['%s weeks ago', 'in %s weeks'],
|
||||
['1 month ago', 'in 1 month'],
|
||||
['%s months ago', 'in %s months'],
|
||||
['1 year ago', 'in 1 year'],
|
||||
['%s years ago', 'in %s years'],
|
||||
][index] as [string, string];
|
||||
};
|
||||
|
||||
register('main', localeFunc as LocaleFunc);
|
||||
|
||||
export default {
|
||||
name: 'UpdatesPanel',
|
||||
export default Vue.extend({
|
||||
name: 'TimeAgo',
|
||||
props: {
|
||||
date: {
|
||||
type: String,
|
||||
|
@ -43,17 +21,48 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
beforeMount() {
|
||||
register(this.defaultLocale, this.localeFunc as LocaleFunc);
|
||||
},
|
||||
methods: {
|
||||
format(props: {date: string, capitalize: boolean}) {
|
||||
const text = format(props.date, 'main');
|
||||
localeFunc(num: number, index: number, totalSec: number): [string, string] {
|
||||
// number: the timeago / timein number;
|
||||
// index: the index of array below;
|
||||
// totalSec: total seconds between date to be formatted and today's date;
|
||||
return [
|
||||
[this.$locale.baseText('timeAgo.justNow'), this.$locale.baseText('timeAgo.rightNow')],
|
||||
[this.$locale.baseText('timeAgo.justNow'), this.$locale.baseText('timeAgo.rightNow')], // ['%s seconds ago', 'in %s seconds'],
|
||||
[this.$locale.baseText('timeAgo.oneMinuteAgo'), this.$locale.baseText('timeAgo.inOneMinute')],
|
||||
[this.$locale.baseText('timeAgo.minutesAgo'), this.$locale.baseText('timeAgo.inMinutes')],
|
||||
[this.$locale.baseText('timeAgo.oneHourAgo'), this.$locale.baseText('timeAgo.inOneHour')],
|
||||
[this.$locale.baseText('timeAgo.hoursAgo'), this.$locale.baseText('timeAgo.inHours')],
|
||||
[this.$locale.baseText('timeAgo.oneDayAgo'), this.$locale.baseText('timeAgo.inOneDay')],
|
||||
[this.$locale.baseText('timeAgo.daysAgo'), this.$locale.baseText('timeAgo.inDays')],
|
||||
[this.$locale.baseText('timeAgo.oneWeekAgo'), this.$locale.baseText('timeAgo.inOneWeek')],
|
||||
[this.$locale.baseText('timeAgo.weeksAgo'), this.$locale.baseText('timeAgo.inWeeks')],
|
||||
[this.$locale.baseText('timeAgo.oneMonthAgo'), this.$locale.baseText('timeAgo.inOneMonth')],
|
||||
[this.$locale.baseText('timeAgo.monthsAgo'), this.$locale.baseText('timeAgo.inMonths')],
|
||||
[this.$locale.baseText('timeAgo.oneYearAgo'), this.$locale.baseText('timeAgo.inOneYear')],
|
||||
[this.$locale.baseText('timeAgo.yearsAgo'), this.$locale.baseText('timeAgo.inYears')],
|
||||
][index] as [string, string];
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['defaultLocale']),
|
||||
format(): string {
|
||||
const text = format(this.date, this.defaultLocale);
|
||||
|
||||
if (!props.capitalize) {
|
||||
if (!this.capitalize) {
|
||||
return text.toLowerCase();
|
||||
}
|
||||
|
||||
return text;
|
||||
},
|
||||
convertToHumanReadableDate,
|
||||
convertDate(): string {
|
||||
const date = new Date(this.date);
|
||||
const epoch = date.getTime() / 1000;
|
||||
return convertToHumanReadableDate(epoch);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,16 +5,26 @@
|
|||
width="520px"
|
||||
>
|
||||
<template slot="header">
|
||||
<span :class="$style.title">We’ve been busy ✨</span>
|
||||
<span :class="$style.title">
|
||||
{{ $locale.baseText('updatesPanel.weVeBeenBusy') }}
|
||||
</span>
|
||||
</template>
|
||||
<template slot="content">
|
||||
<section :class="$style['description']">
|
||||
|
||||
<p v-if="currentVersion">
|
||||
You’re on {{ currentVersion.name }}, which was released
|
||||
<strong><TimeAgo :date="currentVersion.createdAt" /></strong> and is
|
||||
<strong>{{ nextVersions.length }} version{{nextVersions.length > 1 ? "s" : ""}}</strong>
|
||||
behind the latest and greatest n8n
|
||||
{{ $locale.baseText(
|
||||
'updatesPanel.youReOnVersion',
|
||||
{ interpolate: { currentVersionName: currentVersion.name } }
|
||||
) }}
|
||||
<strong><TimeAgo :date="currentVersion.createdAt" /></strong>{{ $locale.baseText('updatesPanel.andIs') }} <strong>{{ $locale.baseText(
|
||||
'updatesPanel.version',
|
||||
{
|
||||
interpolate: {
|
||||
numberOfVersions: nextVersions.length,
|
||||
howManySuffix: nextVersions.length > 1 ? "s" : "",
|
||||
}
|
||||
}
|
||||
)}}</strong> {{ $locale.baseText('updatesPanel.behindTheLatest') }}
|
||||
</p>
|
||||
|
||||
<a
|
||||
|
@ -24,7 +34,9 @@
|
|||
target="_blank"
|
||||
>
|
||||
<font-awesome-icon icon="info-circle"></font-awesome-icon>
|
||||
<span>How to update your n8n version</span>
|
||||
<span>
|
||||
{{ $locale.baseText('updatesPanel.howToUpdateYourN8nVersion') }}
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</section>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div @keydown.stop class="variable-selector-wrapper">
|
||||
<div class="input-wrapper">
|
||||
<n8n-input placeholder="Variable filter..." v-model="variableFilter" ref="inputField" size="small" type="text"></n8n-input>
|
||||
<n8n-input :placeholder="$locale.baseText('variableSelector.variableFilter')" v-model="variableFilter" ref="inputField" size="small" type="text"></n8n-input>
|
||||
</div>
|
||||
|
||||
<div class="result-wrapper">
|
||||
|
@ -525,14 +525,14 @@ export default mixins(
|
|||
|
||||
currentNodeData.push(
|
||||
{
|
||||
name: 'Parameters',
|
||||
name: this.$locale.baseText('variableSelector.parameters'),
|
||||
options: this.sortOptions(this.getNodeParameters(activeNode.name, initialPath, skipParameter, filterText) as IVariableSelectorOption[]),
|
||||
},
|
||||
);
|
||||
|
||||
returnData.push(
|
||||
{
|
||||
name: 'Current Node',
|
||||
name: this.$locale.baseText('variableSelector.currentNode'),
|
||||
options: this.sortOptions(currentNodeData),
|
||||
},
|
||||
);
|
||||
|
@ -546,7 +546,7 @@ export default mixins(
|
|||
let nodeOptions: IVariableSelectorOption[];
|
||||
const upstreamNodes = this.workflow.getParentNodes(activeNode.name, inputName);
|
||||
|
||||
for (const nodeName of Object.keys(this.workflow.nodes)) {
|
||||
for (const [nodeName, node] of Object.entries(this.workflow.nodes)) {
|
||||
// Add the parameters of all nodes
|
||||
// TODO: Later have to make sure that no parameters can be referenced which have expression which use input-data (for nodes which are not parent nodes)
|
||||
|
||||
|
@ -557,7 +557,7 @@ export default mixins(
|
|||
|
||||
nodeOptions = [
|
||||
{
|
||||
name: 'Parameters',
|
||||
name: this.$locale.baseText('variableSelector.parameters'),
|
||||
options: this.sortOptions(this.getNodeParameters(nodeName, `$node["${nodeName}"].parameter`, undefined, filterText)),
|
||||
} as IVariableSelectorOption,
|
||||
];
|
||||
|
@ -570,7 +570,7 @@ export default mixins(
|
|||
if (tempOptions.length) {
|
||||
nodeOptions = [
|
||||
{
|
||||
name: 'Context',
|
||||
name: this.$locale.baseText('variableSelector.context'),
|
||||
options: this.sortOptions(tempOptions),
|
||||
} as IVariableSelectorOption,
|
||||
];
|
||||
|
@ -583,16 +583,21 @@ export default mixins(
|
|||
if (tempOutputData) {
|
||||
nodeOptions.push(
|
||||
{
|
||||
name: 'Output Data',
|
||||
name: this.$locale.baseText('variableSelector.outputData'),
|
||||
options: this.sortOptions(tempOutputData),
|
||||
} as IVariableSelectorOption,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const shortNodeType = this.$locale.shortNodeType(node.type);
|
||||
|
||||
allNodesData.push(
|
||||
{
|
||||
name: nodeName,
|
||||
name: this.$locale.headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: nodeName,
|
||||
}),
|
||||
options: this.sortOptions(nodeOptions),
|
||||
},
|
||||
);
|
||||
|
@ -600,7 +605,7 @@ export default mixins(
|
|||
|
||||
returnData.push(
|
||||
{
|
||||
name: 'Nodes',
|
||||
name: this.$locale.baseText('variableSelector.nodes'),
|
||||
options: this.sortOptions(allNodesData),
|
||||
},
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
|
||||
<el-dropdown trigger="click" @click.stop @command="optionSelected($event, item)" v-if="allowParentSelect === true">
|
||||
<span class="el-dropdown-link clickable" @click.stop>
|
||||
<font-awesome-icon icon="dot-circle" title="Select Item" />
|
||||
<font-awesome-icon icon="dot-circle" :title="$locale.baseText('variableSelectorItem.selectItem')" />
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="operation.command" v-for="operation in itemAddOperations" :key="operation.command">{{operation.displayName}}</el-dropdown-item>
|
||||
|
@ -29,7 +29,7 @@
|
|||
{{item.name}}:
|
||||
<font-awesome-icon icon="dot-circle" title="Select Item" />
|
||||
</div>
|
||||
<div class="item-value">{{ item.value !== undefined?item.value:'--- EMPTY ---' }}</div>
|
||||
<div class="item-value">{{ item.value !== undefined?item.value: $locale.baseText('variableSelectorItem.empty') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
<template functional>
|
||||
<template>
|
||||
<!-- eslint-disable-next-line vue/no-mutating-props -->
|
||||
<a v-if="props.version" :set="version = props.version" :href="version.documentationUrl" target="_blank" :class="$style.card">
|
||||
<a v-if="version" :set="version = version" :href="version.documentationUrl" target="_blank" :class="$style.card">
|
||||
<div :class="$style.header">
|
||||
<div>
|
||||
<div :class="$style.name">
|
||||
Version {{version.name}}
|
||||
{{ `${$locale.baseText('versionCard.version')} ${version.name}` }}
|
||||
</div>
|
||||
<WarningTooltip v-if="version.hasSecurityIssue">
|
||||
<template>
|
||||
This version has a security issue.<br/>It is listed here for completeness.
|
||||
{{ $locale.baseText('versionCard.thisVersionHasASecurityIssue') }}
|
||||
</template>
|
||||
</WarningTooltip>
|
||||
<Badge
|
||||
v-if="version.hasSecurityFix"
|
||||
text="Security update"
|
||||
:text="$locale.baseText('versionCard.securityUpdate')"
|
||||
type="danger"
|
||||
/>
|
||||
<Badge
|
||||
v-if="version.hasBreakingChange"
|
||||
text="Breaking changes"
|
||||
:text="$locale.baseText('versionCard.breakingChanges')"
|
||||
type="warning"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style['release-date']">
|
||||
Released <TimeAgo :date="version.createdAt" />
|
||||
{{ $locale.baseText('versionCard.released') }} <TimeAgo :date="version.createdAt" />
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.divider" v-if="version.description || (version.nodes && version.nodes.length)"></div>
|
||||
|
@ -56,11 +56,11 @@ Vue.component('WarningTooltip', WarningTooltip);
|
|||
|
||||
export default Vue.extend({
|
||||
components: { NodeIcon, TimeAgo, Badge, WarningTooltip },
|
||||
name: 'UpdatesPanel',
|
||||
name: 'VersionCard',
|
||||
props: ['version'],
|
||||
// @ts-ignore
|
||||
nodeName (node: IVersionNode): string {
|
||||
return node !== null ? node.displayName : 'unknown';
|
||||
return node !== null ? node.displayName : this.$locale.baseText('versionCard.unknown');
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
element-loading-spinner="el-icon-loading"
|
||||
:value="workflowActive"
|
||||
@change="activeChanged"
|
||||
:title="workflowActive?'Deactivate Workflow':'Activate Workflow'"
|
||||
:title="workflowActive ? $locale.baseText('workflowActivator.deactivateWorkflow') : $locale.baseText('workflowActivator.activateWorkflow')"
|
||||
:disabled="disabled || loading"
|
||||
:active-color="getActiveColor"
|
||||
inactive-color="#8899AA">
|
||||
|
@ -13,7 +13,7 @@
|
|||
|
||||
<div class="could-not-be-started" v-if="couldNotBeStarted">
|
||||
<n8n-tooltip placement="top">
|
||||
<div @click="displayActivationError" slot="content">The workflow is set to be active but could not be started.<br />Click to display error message.</div>
|
||||
<div @click="displayActivationError" slot="content">{{ $locale.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut') }}</div>
|
||||
<font-awesome-icon @click="displayActivationError" icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
|
@ -79,8 +79,8 @@ export default mixins(
|
|||
async activeChanged (newActiveState: boolean) {
|
||||
if (this.workflowId === undefined) {
|
||||
this.$showMessage({
|
||||
title: 'Problem activating workflow',
|
||||
message: 'The workflow did not get saved yet so can not be set active!',
|
||||
title: this.$locale.baseText('workflowActivator.showMessage.activeChangedWorkflowIdUndefined.title'),
|
||||
message: this.$locale.baseText('workflowActivator.showMessage.activeChangedWorkflowIdUndefined.message'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
|
@ -88,8 +88,8 @@ export default mixins(
|
|||
|
||||
if (this.nodesIssuesExist === true) {
|
||||
this.$showMessage({
|
||||
title: 'Problem activating workflow',
|
||||
message: 'It is only possible to activate a workflow when all issues on all nodes got resolved!',
|
||||
title: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.title'),
|
||||
message: this.$locale.baseText('workflowActivator.showMessage.activeChangedNodesIssuesExistTrue.message'),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
|
@ -105,7 +105,13 @@ export default mixins(
|
|||
// for people because it would activate a different version of the workflow
|
||||
// than the one they can currently see.
|
||||
if (this.dirtyState) {
|
||||
const importConfirm = await this.confirmMessage(`When you activate the workflow all currently unsaved changes of the workflow will be saved.`, 'Activate and save?', 'warning', 'Yes, activate and save!');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText('workflowActivator.confirmMessage.message'),
|
||||
this.$locale.baseText('workflowActivator.confirmMessage.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('workflowActivator.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('workflowActivator.confirmMessage.cancelButtonText'),
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
return;
|
||||
}
|
||||
|
@ -123,7 +129,14 @@ export default mixins(
|
|||
await this.restApi().updateWorkflow(this.workflowId, data);
|
||||
} catch (error) {
|
||||
const newStateName = newActiveState === true ? 'activated' : 'deactivated';
|
||||
this.$showError(error, 'Problem', `There was a problem and the workflow could not be ${newStateName}:`);
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowActivator.showError.title'),
|
||||
this.$locale.baseText(
|
||||
'workflowActivator.showError.message',
|
||||
{ interpolate: { newStateName } },
|
||||
) + ':',
|
||||
);
|
||||
this.loading = false;
|
||||
return;
|
||||
}
|
||||
|
@ -156,16 +169,19 @@ export default mixins(
|
|||
const errorData = await this.restApi().getActivationError(this.workflowId);
|
||||
|
||||
if (errorData === undefined) {
|
||||
errorMessage = 'Sorry there was a problem. No error got found to display.';
|
||||
errorMessage = this.$locale.baseText('workflowActivator.showMessage.displayActivationError.message.errorDataUndefined');
|
||||
} else {
|
||||
errorMessage = `The following error occurred on workflow activation:<br /><i>${errorData.error.message}</i>`;
|
||||
errorMessage = this.$locale.baseText(
|
||||
'workflowActivator.showMessage.displayActivationError.message.errorDataNotUndefined',
|
||||
{ interpolate: { message: errorData.error.message } },
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
errorMessage = 'Sorry there was a problem requesting the error';
|
||||
errorMessage = this.$locale.baseText('workflowActivator.showMessage.displayActivationError.message.catchBlock');
|
||||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Problem activating workflow',
|
||||
title: this.$locale.baseText('workflowActivator.showMessage.displayActivationError.title'),
|
||||
message: errorMessage,
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
<template v-slot:header>
|
||||
<div class="workflows-header">
|
||||
<n8n-heading tag="h1" size="xlarge" class="title">
|
||||
Open Workflow
|
||||
{{ $locale.baseText('workflowOpen.openWorkflow') }}
|
||||
</n8n-heading>
|
||||
<div class="tags-filter">
|
||||
<TagsDropdown
|
||||
placeholder="Filter by tags..."
|
||||
:placeholder="$locale.baseText('workflowOpen.openWorkflow')"
|
||||
:currentTagIds="filterTagIds"
|
||||
:createEnabled="false"
|
||||
@update="updateTagsFilter"
|
||||
|
@ -21,7 +21,7 @@
|
|||
/>
|
||||
</div>
|
||||
<div class="search-filter">
|
||||
<n8n-input placeholder="Search workflows..." ref="inputFieldFilter" v-model="filterText">
|
||||
<n8n-input :placeholder="$locale.baseText('workflowOpen.searchWorkflows')" ref="inputFieldFilter" v-model="filterText">
|
||||
<font-awesome-icon slot="prefix" icon="search"></font-awesome-icon>
|
||||
</n8n-input>
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@
|
|||
|
||||
<template v-slot:content>
|
||||
<el-table class="search-table" :data="filteredWorkflows" stripe @cell-click="openWorkflow" :default-sort = "{prop: 'updatedAt', order: 'descending'}" v-loading="isDataLoading">
|
||||
<el-table-column property="name" label="Name" class-name="clickable" sortable>
|
||||
<el-table-column property="name" :label="$locale.baseText('workflowOpen.name')" class-name="clickable" sortable>
|
||||
<template slot-scope="scope">
|
||||
<div :key="scope.row.id">
|
||||
<span class="name">{{scope.row.name}}</span>
|
||||
|
@ -38,9 +38,9 @@
|
|||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="createdAt" label="Created" class-name="clickable" width="155" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" label="Updated" class-name="clickable" width="155" sortable></el-table-column>
|
||||
<el-table-column label="Active" width="75">
|
||||
<el-table-column property="createdAt" :label="$locale.baseText('workflowOpen.created')" class-name="clickable" width="155" sortable></el-table-column>
|
||||
<el-table-column property="updatedAt" :label="$locale.baseText('workflowOpen.updated')" class-name="clickable" width="155" sortable></el-table-column>
|
||||
<el-table-column :label="$locale.baseText('workflowOpen.active')" width="75">
|
||||
<template slot-scope="scope">
|
||||
<workflow-activator :workflow-active="scope.row.active" :workflow-id="scope.row.id" @workflowActiveChanged="workflowActiveChanged" />
|
||||
</template>
|
||||
|
@ -148,8 +148,8 @@ export default mixins(
|
|||
|
||||
if (data.id === currentWorkflowId) {
|
||||
this.$showMessage({
|
||||
title: 'Already open',
|
||||
message: 'This is the current workflow',
|
||||
title: this.$locale.baseText('workflowOpen.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowOpen.showMessage.message'),
|
||||
type: 'error',
|
||||
duration: 1500,
|
||||
});
|
||||
|
@ -159,7 +159,13 @@ export default mixins(
|
|||
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.message'),
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.confirmButtonText'),
|
||||
this.$locale.baseText('workflowOpen.confirmMessage.cancelButtonText'),
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
return;
|
||||
} else {
|
||||
|
@ -196,7 +202,11 @@ export default mixins(
|
|||
)
|
||||
.catch(
|
||||
(error: Error) => {
|
||||
this.$showError(error, 'Problem loading workflows', 'There was a problem loading the workflows:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowOpen.showError.title'),
|
||||
this.$locale.baseText('workflowOpen.showError.message') + ':',
|
||||
);
|
||||
this.isDataLoading = false;
|
||||
},
|
||||
);
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
:name="WORKFLOW_SETTINGS_MODAL_KEY"
|
||||
width="65%"
|
||||
maxHeight="80%"
|
||||
:title="`Settings for ${workflowName} (#${workflowId})`"
|
||||
:title="$locale.baseText('workflowSettings.settingsFor', { interpolate: { workflowName, workflowId } })"
|
||||
:eventBus="modalBus"
|
||||
:scrollable="true"
|
||||
>
|
||||
|
@ -11,7 +11,7 @@
|
|||
<div v-loading="isLoading" class="workflow-settings">
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Error Workflow:
|
||||
{{ $locale.baseText('workflowSettings.errorWorkflow') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.errorWorkflow"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
|
@ -30,7 +30,7 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Timezone:
|
||||
{{ $locale.baseText('workflowSettings.timezone') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.timezone"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
|
@ -49,14 +49,14 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Save Data Error Execution:
|
||||
{{ $locale.baseText('workflowSettings.saveDataErrorExecution') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.saveDataErrorExecution"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="14" class="ignore-key-press">
|
||||
<n8n-select v-model="workflowSettings.saveDataErrorExecution" placeholder="Select Option" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-select v-model="workflowSettings.saveDataErrorExecution" :placeholder="$locale.baseText('workflowSettings.selectOption')" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-option
|
||||
v-for="option of saveDataErrorExecutionOptions"
|
||||
:key="option.key"
|
||||
|
@ -68,14 +68,14 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Save Data Success Execution:
|
||||
{{ $locale.baseText('workflowSettings.saveDataSuccessExecution') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.saveDataSuccessExecution"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="14" class="ignore-key-press">
|
||||
<n8n-select v-model="workflowSettings.saveDataSuccessExecution" placeholder="Select Option" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-select v-model="workflowSettings.saveDataSuccessExecution" :placeholder="$locale.baseText('workflowSettings.selectOption')" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-option
|
||||
v-for="option of saveDataSuccessExecutionOptions"
|
||||
:key="option.key"
|
||||
|
@ -87,14 +87,14 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Save Manual Executions:
|
||||
{{ $locale.baseText('workflowSettings.saveManualExecutions') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.saveManualExecutions"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="14" class="ignore-key-press">
|
||||
<n8n-select v-model="workflowSettings.saveManualExecutions" placeholder="Select Option" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-select v-model="workflowSettings.saveManualExecutions" :placeholder="$locale.baseText('workflowSettings.selectOption')" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-option
|
||||
v-for="option of saveManualOptions"
|
||||
:key="option.key"
|
||||
|
@ -106,14 +106,14 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Save Execution Progress:
|
||||
{{ $locale.baseText('workflowSettings.saveExecutionProgress') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.saveExecutionProgress"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
</n8n-tooltip>
|
||||
</el-col>
|
||||
<el-col :span="14" class="ignore-key-press">
|
||||
<n8n-select v-model="workflowSettings.saveExecutionProgress" placeholder="Select Option" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-select v-model="workflowSettings.saveExecutionProgress" :placeholder="$locale.baseText('workflowSettings.selectOption')" size="medium" filterable :limit-popper-width="true">
|
||||
<n8n-option
|
||||
v-for="option of saveExecutionProgressOptions"
|
||||
:key="option.key"
|
||||
|
@ -125,7 +125,7 @@
|
|||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Timeout Workflow:
|
||||
{{ $locale.baseText('workflowSettings.timeoutWorkflow') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.executionTimeoutToggle"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
|
@ -140,7 +140,7 @@
|
|||
<div v-if="workflowSettings.executionTimeout > -1">
|
||||
<el-row>
|
||||
<el-col :span="10" class="setting-name">
|
||||
Timeout After:
|
||||
{{ $locale.baseText('workflowSettings.timeoutAfter') + ":" }}
|
||||
<n8n-tooltip class="setting-info" placement="top" >
|
||||
<div slot="content" v-html="helpTexts.executionTimeout"></div>
|
||||
<font-awesome-icon icon="question-circle" />
|
||||
|
@ -148,17 +148,17 @@
|
|||
</el-col>
|
||||
<el-col :span="4">
|
||||
<n8n-input size="medium" :value="timeoutHMS.hours" @input="(value) => setTimeout('hours', value)" :min="0">
|
||||
<template slot="append">hours</template>
|
||||
<template slot="append">{{ $locale.baseText('workflowSettings.hours') }}</template>
|
||||
</n8n-input>
|
||||
</el-col>
|
||||
<el-col :span="4" class="timeout-input">
|
||||
<n8n-input size="medium" :value="timeoutHMS.minutes" @input="(value) => setTimeout('minutes', value)" :min="0" :max="60">
|
||||
<template slot="append">minutes</template>
|
||||
<template slot="append">{{ $locale.baseText('workflowSettings.minutes') }}</template>
|
||||
</n8n-input>
|
||||
</el-col>
|
||||
<el-col :span="4" class="timeout-input">
|
||||
<n8n-input size="medium" :value="timeoutHMS.seconds" @input="(value) => setTimeout('seconds', value)" :min="0" :max="60">
|
||||
<template slot="append">seconds</template>
|
||||
<template slot="append">{{ $locale.baseText('workflowSettings.seconds') }}</template>
|
||||
</n8n-input>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
@ -167,7 +167,7 @@
|
|||
</template>
|
||||
<template v-slot:footer>
|
||||
<div class="action-buttons">
|
||||
<n8n-button label="Save" size="large" float="right" @click="saveSettings" />
|
||||
<n8n-button :label="$locale.baseText('workflowSettings.save')" size="large" float="right" @click="saveSettings" />
|
||||
</div>
|
||||
</template>
|
||||
</Modal>
|
||||
|
@ -207,14 +207,14 @@ export default mixins(
|
|||
return {
|
||||
isLoading: true,
|
||||
helpTexts: {
|
||||
errorWorkflow: 'The workflow to run in case the current one fails.<br />To function correctly that workflow has to contain an "Error Trigger" node!',
|
||||
timezone: 'The timezone in which the workflow should run. Gets for example used by "Cron" node.',
|
||||
saveDataErrorExecution: 'If data data of executions should be saved in case they failed.',
|
||||
saveDataSuccessExecution: 'If data data of executions should be saved in case they succeed.',
|
||||
saveExecutionProgress: 'If data should be saved after each node, allowing you to resume in case of errors from where it stopped. May increase latency.',
|
||||
saveManualExecutions: 'If data data of executions should be saved when started manually from the editor.',
|
||||
executionTimeoutToggle: 'Cancel workflow execution after defined time',
|
||||
executionTimeout: 'After what time the workflow should timeout.',
|
||||
errorWorkflow: this.$locale.baseText('workflowSettings.helpTexts.errorWorkflow'),
|
||||
timezone: this.$locale.baseText('workflowSettings.helpTexts.timezone'),
|
||||
saveDataErrorExecution: this.$locale.baseText('workflowSettings.helpTexts.saveDataErrorExecution'),
|
||||
saveDataSuccessExecution: this.$locale.baseText('workflowSettings.helpTexts.saveDataSuccessExecution'),
|
||||
saveExecutionProgress: this.$locale.baseText('workflowSettings.helpTexts.saveExecutionProgress'),
|
||||
saveManualExecutions: this.$locale.baseText('workflowSettings.helpTexts.saveManualExecutions'),
|
||||
executionTimeoutToggle: this.$locale.baseText('workflowSettings.helpTexts.executionTimeoutToggle'),
|
||||
executionTimeout: this.$locale.baseText('workflowSettings.helpTexts.executionTimeout'),
|
||||
},
|
||||
defaultValues: {
|
||||
timezone: 'America/New_York',
|
||||
|
@ -324,15 +324,24 @@ export default mixins(
|
|||
this.saveDataErrorExecutionOptions, [
|
||||
{
|
||||
key: 'DEFAULT',
|
||||
value: 'Default - ' + (this.defaultValues.saveDataErrorExecution === 'all' ? 'Save' : 'Do not save'),
|
||||
value: this.$locale.baseText(
|
||||
'workflowSettings.saveDataErrorExecutionOptions.defaultSave',
|
||||
{
|
||||
interpolate: {
|
||||
defaultValue: this.defaultValues.saveDataErrorExecution === 'all'
|
||||
? this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.save')
|
||||
: this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotsave'),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'all',
|
||||
value: 'Save',
|
||||
value: this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.save'),
|
||||
},
|
||||
{
|
||||
key: 'none',
|
||||
value: 'Do not save',
|
||||
value: this.$locale.baseText('workflowSettings.saveDataErrorExecutionOptions.doNotSave'),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
@ -343,15 +352,24 @@ export default mixins(
|
|||
this.saveDataSuccessExecutionOptions, [
|
||||
{
|
||||
key: 'DEFAULT',
|
||||
value: 'Default - ' + (this.defaultValues.saveDataSuccessExecution === 'all' ? 'Save' : 'Do not save'),
|
||||
value: this.$locale.baseText(
|
||||
'workflowSettings.saveDataSuccessExecutionOptions.defaultSave',
|
||||
{
|
||||
interpolate: {
|
||||
defaultValue: this.defaultValues.saveDataSuccessExecution === 'all'
|
||||
? this.$locale.baseText('workflowSettings.saveDataSuccessExecutionOptions.save')
|
||||
: this.$locale.baseText('workflowSettings.saveDataSuccessExecutionOptions.doNotSave'),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'all',
|
||||
value: 'Save',
|
||||
value: this.$locale.baseText('workflowSettings.saveDataSuccessExecutionOptions.save'),
|
||||
},
|
||||
{
|
||||
key: 'none',
|
||||
value: 'Do not save',
|
||||
value: this.$locale.baseText('workflowSettings.saveDataSuccessExecutionOptions.doNotSave'),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
@ -362,15 +380,22 @@ export default mixins(
|
|||
this.saveExecutionProgressOptions, [
|
||||
{
|
||||
key: 'DEFAULT',
|
||||
value: 'Default - ' + (this.defaultValues.saveExecutionProgress === true ? 'Yes' : 'No'),
|
||||
value: this.$locale.baseText(
|
||||
'workflowSettings.saveExecutionProgressOptions.defaultSave',
|
||||
{
|
||||
interpolate: {
|
||||
defaultValue: this.defaultValues.saveExecutionProgress ? this.$locale.baseText('workflowSettings.saveExecutionProgressOptions.yes') : this.$locale.baseText('workflowSettings.saveExecutionProgressOptions.no'),
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
key: true,
|
||||
value: 'Yes',
|
||||
value: this.$locale.baseText('workflowSettings.saveExecutionProgressOptions.yes'),
|
||||
},
|
||||
{
|
||||
key: false,
|
||||
value: 'No',
|
||||
value: this.$locale.baseText('workflowSettings.saveExecutionProgressOptions.no'),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
@ -379,15 +404,22 @@ export default mixins(
|
|||
this.saveManualOptions.length = 0;
|
||||
this.saveManualOptions.push({
|
||||
key: 'DEFAULT',
|
||||
value: 'Default - ' + (this.defaultValues.saveManualExecutions === true ? 'Yes' : 'No'),
|
||||
value: this.$locale.baseText(
|
||||
'workflowSettings.saveManualOptions.defaultSave',
|
||||
{
|
||||
interpolate: {
|
||||
defaultValue: this.defaultValues.saveManualExecutions ? this.$locale.baseText('workflowSettings.saveManualOptions.yes') : this.$locale.baseText('workflowSettings.saveManualOptions.no'),
|
||||
},
|
||||
},
|
||||
),
|
||||
});
|
||||
this.saveManualOptions.push({
|
||||
key: true,
|
||||
value: 'Yes',
|
||||
value: this.$locale.baseText('workflowSettings.saveManualOptions.yes'),
|
||||
});
|
||||
this.saveManualOptions.push({
|
||||
key: false,
|
||||
value: 'No',
|
||||
value: this.$locale.baseText('workflowSettings.saveManualOptions.no'),
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -401,12 +433,15 @@ export default mixins(
|
|||
|
||||
let defaultTimezoneValue = timezones[this.defaultValues.timezone] as string | undefined;
|
||||
if (defaultTimezoneValue === undefined) {
|
||||
defaultTimezoneValue = 'Default Timezone not valid!';
|
||||
defaultTimezoneValue = this.$locale.baseText('workflowSettings.defaultTimezoneNotValid');
|
||||
}
|
||||
|
||||
this.timezones.push({
|
||||
key: 'DEFAULT',
|
||||
value: `Default - ${defaultTimezoneValue}`,
|
||||
value: this.$locale.baseText(
|
||||
'workflowSettings.defaultTimezone',
|
||||
{ interpolate: { defaultTimezoneValue } },
|
||||
),
|
||||
});
|
||||
for (const timezone of Object.keys(timezones)) {
|
||||
this.timezones.push({
|
||||
|
@ -430,7 +465,7 @@ export default mixins(
|
|||
// @ts-ignore
|
||||
workflows.unshift({
|
||||
id: undefined as unknown as string,
|
||||
name: '- No Workflow -',
|
||||
name: this.$locale.baseText('workflowSettings.noWorkflow'),
|
||||
});
|
||||
|
||||
Vue.set(this, 'workflows', workflows);
|
||||
|
@ -449,14 +484,33 @@ export default mixins(
|
|||
: -1;
|
||||
|
||||
if (data.settings!.executionTimeout === 0) {
|
||||
this.$showError(new Error('timeout is activated but set to 0'), 'Problem saving settings', 'There was a problem saving the settings:');
|
||||
this.$showError(
|
||||
new Error(this.$locale.baseText('workflowSettings.showError.saveSettings1.errorMessage')),
|
||||
this.$locale.baseText('workflowSettings.showError.saveSettings1.title'),
|
||||
this.$locale.baseText('workflowSettings.showError.saveSettings1.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
if (data.settings!.executionTimeout > this.workflowSettings.maxExecutionTimeout) {
|
||||
const { hours, minutes, seconds } = this.convertToHMS(this.workflowSettings.maxExecutionTimeout as number);
|
||||
this.$showError(new Error(`Maximum Timeout is: ${hours} hours, ${minutes} minutes, ${seconds} seconds`), 'Problem saving settings', 'Set timeout is exceeding the maximum timeout!');
|
||||
this.$showError(
|
||||
new Error(
|
||||
this.$locale.baseText(
|
||||
'workflowSettings.showError.saveSettings2.errorMessage',
|
||||
{
|
||||
interpolate: {
|
||||
hours: hours.toString(),
|
||||
minutes: minutes.toString(),
|
||||
seconds: seconds.toString(),
|
||||
},
|
||||
},
|
||||
),
|
||||
),
|
||||
this.$locale.baseText('workflowSettings.showError.saveSettings2.title'),
|
||||
this.$locale.baseText('workflowSettings.showError.saveSettings2.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
delete data.settings!.maxExecutionTimeout;
|
||||
|
@ -466,7 +520,11 @@ export default mixins(
|
|||
try {
|
||||
await this.restApi().updateWorkflow(this.$route.params.name, data);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem saving settings', 'There was a problem saving the settings:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowSettings.showError.saveSettings3.title'),
|
||||
this.$locale.baseText('workflowSettings.showError.saveSettings3.message') + ':',
|
||||
);
|
||||
this.isLoading = false;
|
||||
return;
|
||||
}
|
||||
|
@ -486,8 +544,8 @@ export default mixins(
|
|||
this.isLoading = false;
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Settings saved',
|
||||
message: 'The workflow settings got saved!',
|
||||
title: this.$locale.baseText('workflowSettings.showMessage.saveSettings.title'),
|
||||
message: this.$locale.baseText('workflowSettings.showMessage.saveSettings.message'),
|
||||
type: 'success',
|
||||
});
|
||||
|
||||
|
|
|
@ -22,23 +22,23 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||
displayTimer (msPassed: number, showMs = false): string {
|
||||
if (msPassed < 60000) {
|
||||
if (showMs === false) {
|
||||
return `${Math.floor(msPassed / 1000)} sec.`;
|
||||
return `${this.$locale.number(Math.floor(msPassed / 1000), 'decimal')} ${this.$locale.baseText('genericHelpers.sec')}`;
|
||||
}
|
||||
|
||||
return `${msPassed / 1000} sec.`;
|
||||
return `${this.$locale.number(msPassed / 1000, 'decimal')} ${this.$locale.baseText('genericHelpers.sec')}`;
|
||||
}
|
||||
|
||||
const secondsPassed = Math.floor(msPassed / 1000);
|
||||
const minutesPassed = Math.floor(secondsPassed / 60);
|
||||
const secondsLeft = (secondsPassed - (minutesPassed * 60)).toString().padStart(2, '0');
|
||||
|
||||
return `${minutesPassed}:${secondsLeft} min.`;
|
||||
return `${this.$locale.number(minutesPassed, 'decimal')}:${this.$locale.number(secondsPassed, 'decimal')} ${this.$locale.baseText('genericHelpers.min')}`;
|
||||
},
|
||||
editAllowedCheck (): boolean {
|
||||
if (this.isReadOnly) {
|
||||
this.$showMessage({
|
||||
title: 'Workflow can not be changed!',
|
||||
message: `The workflow can not be edited as a past execution gets displayed. To make changed either open the original workflow of which the execution gets displayed or save it under a new name first.`,
|
||||
// title: 'Workflow can not be changed!',
|
||||
title: this.$locale.baseText('genericHelpers.showMessage.title'),
|
||||
message: this.$locale.baseText('genericHelpers.showMessage.message'),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
|
@ -57,7 +57,7 @@ export const genericHelpers = mixins(showMessage).extend({
|
|||
this.loadingService = this.$loading(
|
||||
{
|
||||
lock: true,
|
||||
text: text || 'Loading',
|
||||
text: text || this.$locale.baseText('genericHelpers.loading'),
|
||||
spinner: 'el-icon-loading',
|
||||
background: 'rgba(255, 255, 255, 0.8)',
|
||||
},
|
||||
|
|
|
@ -179,6 +179,7 @@ export const nodeBase = mixins(
|
|||
hover: false,
|
||||
showOutputLabel: nodeTypeData.outputs.length === 1,
|
||||
size: nodeTypeData.outputs.length >= 3 ? 'small' : 'medium',
|
||||
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
||||
},
|
||||
endpointHoverStyle: {
|
||||
fill: getStyleTokenValue('--color-primary'),
|
||||
|
|
|
@ -263,8 +263,8 @@ export const pushConnection = mixins(
|
|||
// Workflow did execute without a problem
|
||||
this.$titleSet(workflow.name as string, 'IDLE');
|
||||
this.$showMessage({
|
||||
title: 'Workflow was executed',
|
||||
message: 'Workflow was executed successfully!',
|
||||
title: this.$locale.baseText('pushConnection.showMessage.title'),
|
||||
message: this.$locale.baseText('pushConnection.showMessage.message'),
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
IWorkflowShortResponse,
|
||||
IRestApi,
|
||||
IWorkflowDataUpdate,
|
||||
INodeTranslationHeaders,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
IDataObject,
|
||||
|
@ -78,6 +79,10 @@ export const restApi = Vue.extend({
|
|||
return self.restApi().makeRestApiRequest('POST', `/executions-current/${executionId}/stop`);
|
||||
},
|
||||
|
||||
getNodeTranslationHeaders: (): Promise<INodeTranslationHeaders> => {
|
||||
return self.restApi().makeRestApiRequest('GET', '/node-translation-headers');
|
||||
},
|
||||
|
||||
// Returns all node-types
|
||||
getNodeTypes: (onlyLatest = false): Promise<INodeTypeDescription[]> => {
|
||||
return self.restApi().makeRestApiRequest('GET', `/node-types`, {onlyLatest});
|
||||
|
|
|
@ -131,11 +131,11 @@ export const showMessage = mixins(externalHooks).extend({
|
|||
this.$telemetry.track('Instance FE emitted error', { error_title: title, error_description: message, error_message: error.message, workflow_id: this.$store.getters.workflowId });
|
||||
},
|
||||
|
||||
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText = 'OK', cancelButtonText = 'Cancel'): Promise<boolean> {
|
||||
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText?: string, cancelButtonText?: string): Promise<boolean> {
|
||||
try {
|
||||
const options: ElMessageBoxOptions = {
|
||||
confirmButtonText,
|
||||
cancelButtonText,
|
||||
confirmButtonText: confirmButtonText || this.$locale.baseText('showMessage.ok'),
|
||||
cancelButtonText: cancelButtonText || this.$locale.baseText('showMessage.cancel'),
|
||||
dangerouslyUseHTMLString: true,
|
||||
...(type && { type }),
|
||||
};
|
||||
|
@ -173,7 +173,7 @@ export const showMessage = mixins(externalHooks).extend({
|
|||
<summary
|
||||
style="color: #ff6d5a; font-weight: bold; cursor: pointer;"
|
||||
>
|
||||
Show Details
|
||||
${this.$locale.baseText('showMessage.showDetails')}
|
||||
</summary>
|
||||
<p>${node.name}: ${errorDescription}</p>
|
||||
</details>
|
||||
|
|
|
@ -49,7 +49,7 @@ import { showMessage } from '@/components/mixins/showMessage';
|
|||
import { isEqual } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { v4 as uuidv4} from 'uuid';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const workflowHelpers = mixins(
|
||||
externalHooks,
|
||||
|
@ -478,8 +478,8 @@ export const workflowHelpers = mixins(
|
|||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Problem saving workflow',
|
||||
message: `There was a problem saving the workflow: "${e.message}"`,
|
||||
title: this.$locale.baseText('workflowHelpers.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowHelpers.showMessage.message') + `: "${e.message}"`,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
|
@ -552,8 +552,8 @@ export const workflowHelpers = mixins(
|
|||
this.$store.commit('removeActiveAction', 'workflowSaving');
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Problem saving workflow',
|
||||
message: `There was a problem saving the workflow: "${e.message}"`,
|
||||
title: this.$locale.baseText('workflowHelpers.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowHelpers.showMessage.message') + `: "${e.message}"`,
|
||||
type: 'error',
|
||||
});
|
||||
|
||||
|
|
|
@ -31,7 +31,9 @@ export const workflowRun = mixins(
|
|||
if (this.$store.getters.pushConnectionActive === false) {
|
||||
// Do not start if the connection to server is not active
|
||||
// because then it can not receive the data as it executes.
|
||||
throw new Error('No active connection to server. It is maybe down.');
|
||||
throw new Error(
|
||||
this.$locale.baseText('workflowRun.noActiveConnectionToTheServer'),
|
||||
);
|
||||
}
|
||||
|
||||
this.$store.commit('addActiveAction', 'workflowRunning');
|
||||
|
@ -89,8 +91,8 @@ export const workflowRun = mixins(
|
|||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow can not be executed',
|
||||
message: 'The workflow has issues. Please fix them first:<br /> - ' + errorMessages.join('<br /> - '),
|
||||
title: this.$locale.baseText('workflowRun.showMessage.title'),
|
||||
message: this.$locale.baseText('workflowRun.showMessage.message') + ':<br /> - ' + errorMessages.join('<br /> - '),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
|
@ -200,7 +202,11 @@ export const workflowRun = mixins(
|
|||
return runWorkflowApiResponse;
|
||||
} catch (error) {
|
||||
this.$titleSet(workflow.name as string, 'ERROR');
|
||||
this.$showError(error, 'Problem running workflow', 'There was a problem running the workflow:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('workflowRun.showError.title'),
|
||||
this.$locale.baseText('workflowRun.showError.message'),
|
||||
);
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
|
|
@ -74,7 +74,7 @@ export const CUSTOM_NODES_CATEGORY = 'Custom Nodes';
|
|||
export const SUBCATEGORY_DESCRIPTIONS: {
|
||||
[category: string]: { [subcategory: string]: string };
|
||||
} = {
|
||||
'Core Nodes': {
|
||||
'Core Nodes': { // this - all subkeys are set from codex
|
||||
Flow: 'Branches, core triggers, merge data',
|
||||
Files: 'Work with CSV, XML, text, images etc.',
|
||||
'Data Transformation': 'Manipulate data fields, run code',
|
||||
|
|
|
@ -18,6 +18,7 @@ import router from './router';
|
|||
|
||||
import { runExternalHook } from './components/mixins/externalHooks';
|
||||
import { TelemetryPlugin } from './plugins/telemetry';
|
||||
import { I18nPlugin } from './plugins/i18n';
|
||||
|
||||
import { store } from './store';
|
||||
|
||||
|
@ -27,6 +28,7 @@ router.afterEach((to, from) => {
|
|||
});
|
||||
|
||||
Vue.use(TelemetryPlugin);
|
||||
Vue.use((vue) => I18nPlugin(vue, store));
|
||||
|
||||
new Vue({
|
||||
router,
|
||||
|
|
|
@ -64,6 +64,7 @@ const module: Module<ISettingsState, IRootState> = {
|
|||
context.commit('setInstanceId', settings.instanceId, {root: true});
|
||||
context.commit('setOauthCallbackUrls', settings.oauthCallbackUrls, {root: true});
|
||||
context.commit('setN8nMetadata', settings.n8nMetadata || {}, {root: true});
|
||||
context.commit('setDefaultLocale', settings.defaultLocale, {root: true});
|
||||
context.commit('versions/setVersionNotificationSettings', settings.versionNotifications, {root: true});
|
||||
context.commit('setTelemetry', settings.telemetry, {root: true});
|
||||
|
||||
|
|
|
@ -158,7 +158,7 @@ body {
|
|||
|
||||
// Notification
|
||||
.el-notification {
|
||||
border-radius: 0;
|
||||
border-radius: 4px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -461,6 +461,11 @@
|
|||
this.size = endpointStyle.size || this.size;
|
||||
this.showOutputLabel = !!endpointStyle.showOutputLabel;
|
||||
|
||||
if (this.hoverMessage !== endpointStyle.hoverMessage) {
|
||||
this.hoverMessage = endpointStyle.hoverMessage;
|
||||
message.innerHTML = endpointStyle.hoverMessage;
|
||||
}
|
||||
|
||||
if (this.size !== 'medium') {
|
||||
container.classList.add(this.size);
|
||||
}
|
||||
|
|
595
packages/editor-ui/src/plugins/i18n/docs/README.md
Normal file
|
@ -0,0 +1,595 @@
|
|||
# i18n in n8n
|
||||
|
||||
## Scope
|
||||
|
||||
n8n allows for internalization of the majority of UI text:
|
||||
|
||||
- base text, e.g. menu display items in the left-hand sidebar menu,
|
||||
- node text, e.g. parameter display names and placeholders in the node view,
|
||||
- header text, e.g. node display names and descriptions in the nodes panel.
|
||||
|
||||
Currently, n8n does _not_ allow for internalization of:
|
||||
|
||||
- messages from outside the `editor-ui` package, e.g. `No active database connection`,
|
||||
- node subtitles, e.g. `create: user` or `getAll: post` below the node name on the canvas,
|
||||
- new version notification contents in the updates panel, e.g. `Includes node enhancements`.
|
||||
|
||||
## Locale identifiers
|
||||
|
||||
A locale identifier is a language code compatible with the [`Accept-Language` header](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Language), e.g. `de` (German), `es` (Spanish), `ja` (Japanese). Regional variants of locale identifiers are not supported, i.e. use `de`, not `de-AT`. For a list of all locale identifiers, refer to the [639-1 column in the ISO 639-1 codes article](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
|
||||
|
||||
By default, n8n runs in the `en` (English) locale. To have it run in a different locale, set the `N8N_DEFAULT_LOCALE` environment variable. If it has been set and is not `en`, n8n will use the UI strings for that locale - for any untranslated UI strings, n8n will automatically fall back to `en`.
|
||||
|
||||
```sh
|
||||
export N8N_DEFAULT_LOCALE=de
|
||||
npm run start
|
||||
```
|
||||
|
||||
## Base text
|
||||
|
||||
Base text is directly rendered with no dependencies. Base text is supplied by the user in one file per locale in the `/editor-ui` package.
|
||||
|
||||
### Locating base text
|
||||
|
||||
Each base text file is located at `/packages/editor-ui/src/plugins/i18n/locales/{localeIdentifier}.json` and exports an object where keys are Vue component names (and their containing dirs if any) and references to parts of those Vue components.
|
||||
|
||||
```json
|
||||
"nodeCreator": {
|
||||
"categoryNames": {
|
||||
"analytics": "🇩🇪 Analytics",
|
||||
"communication": "🇩🇪 Communication",
|
||||
"coreNodes": "🇩🇪 Core Nodes",
|
||||
```
|
||||
|
||||
### Translating base text
|
||||
|
||||
1. For the new locale identifier, e.g. `de`, copy the `en` base text and rename it:
|
||||
|
||||
```sh
|
||||
cp ./packages/editor-ui/src/plugins/i18n/locales/en.json ./packages/editor-ui/src/plugins/i18n/locales/de.json
|
||||
```
|
||||
|
||||
2. Check in the UI for a base text string to translate, and find it in the newly created base text file.
|
||||
|
||||
> **Note**: If you cannot find a string in the new base text file, either it does not belong to base text (i.e., the string might be part of header text, credential text, or node text), or the string might belong to the backend, where i18n is currently unsupported.
|
||||
|
||||
3. Translate the string value - do not change the key. In the examples below, a string starting with 🇩🇪 stands for a translated string.
|
||||
|
||||
Optionally, remove any untranslated strings from the new base text file. Untranslated strings in the new base text file will automatically fall back to the `en` base text file.
|
||||
|
||||
#### Reusable base text
|
||||
|
||||
As a convenience, the base text file may contain the special key `reusableBaseText` to share strings between translations. For more information, refer to Vue i18n's [linked locale messages](https://kazupon.github.io/vue-i18n/guide/messages.html#linked-locale-messages).
|
||||
|
||||
```json
|
||||
{
|
||||
"reusableBaseText": {
|
||||
"save": "🇩🇪 Save",
|
||||
},
|
||||
"duplicateWorkflowDialog": {
|
||||
"enterWorkflowName": "🇩🇪 Enter workflow name",
|
||||
"save": "@:reusableBaseText.save",
|
||||
},
|
||||
"saveButton": {
|
||||
"save": "@:reusableBaseText.save",
|
||||
"saving": "🇩🇪 Saving",
|
||||
"saved": "🇩🇪 Saved",
|
||||
},
|
||||
```
|
||||
|
||||
As a convenience, the base text file may also contain the special key `numberFormats` to localize numbers. For more information, refer to Vue i18n's [number localization](https://kazupon.github.io/vue-i18n/guide/number.html#number-localization).
|
||||
|
||||
```json
|
||||
{
|
||||
"numberFormats": {
|
||||
"decimal": {
|
||||
"style": "decimal",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Interpolation
|
||||
|
||||
Some base text strings use [interpolation](https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting) with a variable in curly braces, e.g. `Execution ID {activeExecutionId} was stopped`. In case of interpolation, the translated string must not modify the variable: `Die Ausführung mit der ID {activeExecutionId} wurde gestoppt`.
|
||||
|
||||
## Dynamic text
|
||||
|
||||
Dynamic text is **text that relies on node-related data** in order to be rendered. Node-related data is supplied by the user in multiple files in the `/nodes-base` package. Dynamic text is mostly visible in the node view, i.e. the node on the canvas and the node parameters modal.
|
||||
|
||||
### Locating dynamic text
|
||||
|
||||
Dynamic text is divided into files located in `/translations` dirs alongside the translated nodes:
|
||||
|
||||
```
|
||||
GitHub
|
||||
├── GitHub.node.ts
|
||||
├── GitHubTrigger.node.ts
|
||||
└── translations
|
||||
├── de.json
|
||||
├── es.json
|
||||
└── ja.json
|
||||
```
|
||||
|
||||
Each node translation file may contain the translations for one or both (regular and trigger) nodes.
|
||||
|
||||
For nodes in grouping dirs, e.g. `Google`, `Aws`, and `Microsoft`, locate the `/translations` dir alongside the `*.node.ts` file:
|
||||
|
||||
```
|
||||
Google
|
||||
└── Drive
|
||||
├── GoogleDrive.node.ts
|
||||
└── translations
|
||||
├── de.json
|
||||
├── es.json
|
||||
└── ja.json
|
||||
```
|
||||
|
||||
For nodes in versioned dirs, locate the `/translations` dir alongside the versioned `*.node.ts` file:
|
||||
|
||||
```
|
||||
Mattermost
|
||||
└── Mattermost.node.ts
|
||||
└── v1
|
||||
├── MattermostV1.node.ts
|
||||
├── actions
|
||||
├── methods
|
||||
├── transport
|
||||
└── translations
|
||||
├── de.json
|
||||
├── es.json
|
||||
└── ja.json
|
||||
```
|
||||
|
||||
### Translating dynamic text
|
||||
|
||||
> **Note**: In the examples below, the node source is located at `/packages/nodes-base/nodes/Github/GitHub.node.ts` and the node translation is located at `/packages/nodes-base/nodes/Github/translations/de.json`.
|
||||
|
||||
Each node translation is an object with a key that matches the node's `description.name`:
|
||||
|
||||
```ts
|
||||
export class Github implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'GitHub',
|
||||
description: 'Consume GitHub API',
|
||||
name: 'github', // key to use in translation
|
||||
icon: 'file:github.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {}, // key from node's description.name
|
||||
"githubTrigger": {}, // key from node's description.name
|
||||
}
|
||||
```
|
||||
|
||||
The object inside allows for three keys: `header`, `credentialsModal` and `nodeView`. These are the _sections_ of each node translation:
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {},
|
||||
"nodeView": {},
|
||||
},
|
||||
"githubTrigger": {
|
||||
"header": {},
|
||||
"credentialsModal": {},
|
||||
"nodeView": {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: These three keys as well as all keys described below are optional. Remember that, in case of missing sections or missing translations, n8n will fall back to the `en` locale.
|
||||
|
||||
#### `header` section
|
||||
|
||||
The `header` section points to an object that may contain only two keys, `displayName` and `description`, matching the node's `description.displayName` and `description.description`. These are used in the nodes panel, in the node view and in the node credentials modal.
|
||||
|
||||
```ts
|
||||
export class Github implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'GitHub', // key to use in translation
|
||||
description: 'Consume GitHub API', // key to use in translation
|
||||
name: 'github',
|
||||
icon: 'file:github.svg',
|
||||
group: ['input'],
|
||||
version: 1,
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {
|
||||
"displayName": "🇩🇪 GitHub",
|
||||
"description": "🇩🇪 Consume GitHub API",
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Header text is used wherever the node's display name and description are needed:
|
||||
|
||||
<p align="center">
|
||||
<img src="img/header1.png" width="400">
|
||||
<img src="img/header2.png" width="200">
|
||||
<img src="img/header3.png" width="400">
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<img src="img/header4.png" width="400">
|
||||
<img src="img/header5.png" width="500">
|
||||
</p>
|
||||
|
||||
#### `credentialsModal` section
|
||||
|
||||
> **Note**: In the examples below, the node credential source is located at `/packages/nodes-base/credentials/GithubApi.credentials.ts`.
|
||||
|
||||
The `credentialsModal` section points to an object containing a key that matches the node credential `name`.
|
||||
|
||||
```ts
|
||||
export class GithubApi implements ICredentialType {
|
||||
name = 'githubApi'; // key to use in translation
|
||||
displayName = 'Github API';
|
||||
documentationUrl = 'github';
|
||||
properties: INodeProperties[] = [
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {
|
||||
"githubApi": {} // key from node credential name
|
||||
},
|
||||
"nodeView": {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The node credential `name` key points to an object containing translation keys that match the node's credential parameter names:
|
||||
|
||||
```ts
|
||||
export class GithubApi implements ICredentialType {
|
||||
name = 'githubApi';
|
||||
displayName = 'Github API';
|
||||
documentationUrl = 'github';
|
||||
properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'Github Server',
|
||||
name: 'server', // key to use in translation
|
||||
type: 'string',
|
||||
default: 'https://api.github.com',
|
||||
description: 'The server to connect to. Only has to be set if Github Enterprise is used.',
|
||||
},
|
||||
{
|
||||
displayName: 'User',
|
||||
name: 'user', // key to use in translation
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
displayName: 'Access Token',
|
||||
name: 'accessToken', // key to use in translation
|
||||
type: 'string',
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {
|
||||
"githubApi": {
|
||||
"server": {} // key from node credential parameter name
|
||||
"user": {} // key from node credential parameter name
|
||||
"accessToken": {} // key from node credential parameter name
|
||||
},
|
||||
},
|
||||
"nodeView": {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The object for each node credential parameter allows for the keys `displayName`, `description`, and `placeholder`.
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {
|
||||
"githubApi": {
|
||||
"server": {
|
||||
"displayName": "🇩🇪 Github Server",
|
||||
"description": "🇩🇪 The server to connect to. Only has to be set if Github Enterprise is used.",
|
||||
},
|
||||
"user": {
|
||||
"placeholder": "🇩🇪 Hans",
|
||||
},
|
||||
"accessToken": {
|
||||
"placeholder": "🇩🇪 123",
|
||||
},
|
||||
},
|
||||
},
|
||||
"nodeView": {},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="img/cred.png">
|
||||
</p>
|
||||
|
||||
#### `nodeView` section
|
||||
|
||||
The `nodeView` section points to an object containing translation keys that match the node's operational parameters.
|
||||
|
||||
```ts
|
||||
export class Github implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'GitHub',
|
||||
name: 'github',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource', // key to use in translation
|
||||
type: 'options',
|
||||
options: [],
|
||||
default: 'issue',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {},
|
||||
"nodeView": {
|
||||
"resource": {}, // key from node parameter name
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
> **Note**: Other than in the `*.node.ts` file, operational parameters may also be found in `*Description.ts` files in the same dir, e.g. `UserDescription.ts`.
|
||||
|
||||
A node parameter allows for different translation keys depending on parameter type.
|
||||
|
||||
#### `string`, `number` and `boolean` parameters
|
||||
|
||||
Allowed keys: `displayName`, `description`, and `placeholder`.
|
||||
|
||||
```ts
|
||||
{
|
||||
displayName: 'Repository Owner',
|
||||
name: 'owner', // key to use in translation
|
||||
type: 'string',
|
||||
required: true,
|
||||
placeholder: 'n8n-io',
|
||||
description: 'Owner of the repository.',
|
||||
},
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {},
|
||||
"nodeView": {
|
||||
"owner": { // key from node parameter name
|
||||
"displayName": "🇩🇪 Repository Owner",
|
||||
"placeholder": "🇩🇪 n8n-io",
|
||||
"description": "🇩🇪 Owner of the repository.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="img/node1.png" width="400">
|
||||
</p>
|
||||
|
||||
#### `options` parameter
|
||||
|
||||
Allowed keys: `displayName`, `description`, and `placeholder`.
|
||||
|
||||
Allowed subkeys: `options.{optionName}.displayName` and `options.{optionName}.description`.
|
||||
|
||||
```ts
|
||||
{
|
||||
displayName: 'Resource',
|
||||
name: 'resource',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'File',
|
||||
value: 'file', // key to use in translation
|
||||
},
|
||||
{
|
||||
name: 'Issue',
|
||||
value: 'issue', // key to use in translation
|
||||
},
|
||||
],
|
||||
default: 'issue',
|
||||
description: 'The resource to operate on.',
|
||||
},
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {},
|
||||
"nodeView": {
|
||||
"resource": {
|
||||
"displayName": "🇩🇪 Resource",
|
||||
"description": "🇩🇪 The resource to operate on.",
|
||||
"options": {
|
||||
"file": { // key from node parameter options name
|
||||
"displayName": "🇩🇪 File",
|
||||
},
|
||||
"issue": { // key from node parameter options name
|
||||
"displayName": "🇩🇪 Issue",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="img/node2.png" width="400">
|
||||
</p>
|
||||
|
||||
#### `collection` and `fixedCollection` parameters
|
||||
|
||||
Allowed keys: `displayName`, `description`, `placeholder`, and `multipleValueButtonText`.
|
||||
|
||||
```ts
|
||||
{
|
||||
displayName: 'Labels',
|
||||
name: 'labels', // key to use in translation
|
||||
type: 'collection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
multipleValueButtonText: 'Add Label',
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
operation: [
|
||||
'create',
|
||||
],
|
||||
resource: [
|
||||
'issue',
|
||||
],
|
||||
},
|
||||
},
|
||||
default: { 'label': '' },
|
||||
options: [
|
||||
{
|
||||
displayName: 'Label',
|
||||
name: 'label', // key to use in translation
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'Label to add to issue.',
|
||||
},
|
||||
],
|
||||
},
|
||||
```
|
||||
|
||||
To reduce nesting and to share translations, a parameter inside a collection's or fixed collection's `options` parameter sits at the same level of nesting as the containing collection in the `nodeView` section:
|
||||
|
||||
```json
|
||||
{
|
||||
"github": {
|
||||
"header": {},
|
||||
"credentialsModal": {},
|
||||
"nodeView": {
|
||||
// collection
|
||||
"labels": {
|
||||
"displayName": "🇩🇪 Labels",
|
||||
"multipleValueButtonText": "🇩🇪 Add Label",
|
||||
},
|
||||
// collection item - same level of nesting
|
||||
"label": {
|
||||
"displayName": "🇩🇪 Label",
|
||||
"description": "🇩🇪 Label to add to issue.",
|
||||
},
|
||||
|
||||
// fixed collection
|
||||
"additionalParameters": {
|
||||
"displayName": "🇩🇪 Additional Fields",
|
||||
"options": {
|
||||
"author": {
|
||||
"displayName": "🇩🇪 Author",
|
||||
},
|
||||
},
|
||||
},
|
||||
// fixed collection item - same level of nesting
|
||||
"author": {
|
||||
"displayName": "🇩🇪 Author",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
<p align="center">
|
||||
<img src="img/node4.png" width="400">
|
||||
</p>
|
||||
|
||||
> **Note**: In case of deep nesting, i.e. a child of a child of a `collection` and `fixedCollection` parameter, the deeply nested child in principle should be translatable at the same level of nesting as the `collection` and `fixedCollection` parameter, but this has not been fully tested for this first release.
|
||||
|
||||
#### Reusable dynamic text
|
||||
|
||||
The base text file may contain the special key `reusableDynamicText`, allowing for a node parameter to be translated once and reused in all other node parameter translations.
|
||||
|
||||
Currently only the keys `oauth.clientId` and `oauth.clientSecret` are supported as a PoC - these two translations will be reused in all node credential parameters.
|
||||
|
||||
```json
|
||||
{
|
||||
"reusableDynamicText": {
|
||||
"oauth2": {
|
||||
"clientId": "🇩🇪 Client ID",
|
||||
"clientSecret": "🇩🇪 Client Secret",
|
||||
```
|
||||
|
||||
# Building translations
|
||||
|
||||
## Base text
|
||||
|
||||
When translating a base text file at `/packages/editor-ui/src/plugins/i18n/locales/{localeIdentifier}.json`:
|
||||
|
||||
1. Open a terminal:
|
||||
|
||||
```sh
|
||||
export N8N_DEFAULT_LOCALE=de
|
||||
npm run start
|
||||
```
|
||||
|
||||
2. Open another terminal:
|
||||
|
||||
```sh
|
||||
export N8N_DEFAULT_LOCALE=de
|
||||
cd packages/editor-ui
|
||||
npm run dev
|
||||
```
|
||||
|
||||
Changing the base text file will trigger a rebuild of the client at `http://localhost:8080`.
|
||||
|
||||
## Dynamic text
|
||||
|
||||
When translating a dynamic text file at `/packages/nodes-base/nodes/{node}/translations/{localeIdentifier}.json`,
|
||||
|
||||
1. Open a terminal:
|
||||
|
||||
```sh
|
||||
export N8N_DEFAULT_LOCALE=de
|
||||
npm run start
|
||||
```
|
||||
|
||||
2. Open another terminal:
|
||||
|
||||
```sh
|
||||
export N8N_DEFAULT_LOCALE=de
|
||||
cd packages/nodes-base
|
||||
npm run build:translations
|
||||
npm run watch
|
||||
```
|
||||
|
||||
After changing the dynamic text file:
|
||||
|
||||
1. Stop and restart the first terminal.
|
||||
2. Refresh the browser at `http://localhost:5678`
|
||||
|
||||
If a `headerText` section was changed, re-run `npm run build:translations` in `/nodes-base`.
|
||||
|
||||
> **Note**: To translate base and dynamic text simultaneously, run three terminals following the steps from both sections (first terminal running only once) and browse `http://localhost:8080`.
|
BIN
packages/editor-ui/src/plugins/i18n/docs/img/cred.png
Normal file
After Width: | Height: | Size: 857 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/header1.png
Normal file
After Width: | Height: | Size: 253 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/header2.png
Normal file
After Width: | Height: | Size: 161 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/header3.png
Normal file
After Width: | Height: | Size: 239 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/header4.png
Normal file
After Width: | Height: | Size: 344 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/header5.png
Normal file
After Width: | Height: | Size: 296 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/node1.png
Normal file
After Width: | Height: | Size: 38 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/node2.png
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
packages/editor-ui/src/plugins/i18n/docs/img/node4.png
Normal file
After Width: | Height: | Size: 82 KiB |
330
packages/editor-ui/src/plugins/i18n/index.ts
Normal file
|
@ -0,0 +1,330 @@
|
|||
import _Vue from "vue";
|
||||
import axios from 'axios';
|
||||
import VueI18n from 'vue-i18n';
|
||||
import { Store } from "vuex";
|
||||
import Vue from 'vue';
|
||||
import { INodeTranslationHeaders, IRootState } from '@/Interface';
|
||||
const englishBaseText = require('./locales/en');
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
const REUSABLE_DYNAMIC_TEXT_KEY = 'reusableDynamicText';
|
||||
const CREDENTIALS_MODAL_KEY = 'credentialsModal';
|
||||
const NODE_VIEW_KEY = 'nodeView';
|
||||
|
||||
export function I18nPlugin(vue: typeof _Vue, store: Store<IRootState>): void {
|
||||
const i18n = new I18nClass(store);
|
||||
|
||||
Object.defineProperty(vue, '$locale', {
|
||||
get() { return i18n; },
|
||||
});
|
||||
|
||||
Object.defineProperty(vue.prototype, '$locale', {
|
||||
get() { return i18n; },
|
||||
});
|
||||
}
|
||||
|
||||
export class I18nClass {
|
||||
$store: Store<IRootState>;
|
||||
|
||||
constructor(store: Store<IRootState>) {
|
||||
this.$store = store;
|
||||
}
|
||||
|
||||
private get i18n(): VueI18n {
|
||||
return i18nInstance;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// helper methods
|
||||
// ----------------------------------
|
||||
|
||||
exists(key: string) {
|
||||
return this.i18n.te(key);
|
||||
}
|
||||
|
||||
number(value: number, options: VueI18n.FormattedNumberPartType) {
|
||||
return this.i18n.n(value, options);
|
||||
}
|
||||
|
||||
shortNodeType(longNodeType: string) {
|
||||
return longNodeType.replace('n8n-nodes-base.', '');
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
// render methods
|
||||
// ----------------------------------
|
||||
|
||||
/**
|
||||
* Render a string of base text, i.e. a string with a fixed path to the localized value in the base text object. Optionally allows for [interpolation](https://kazupon.github.io/vue-i18n/guide/formatting.html#named-formatting) when the localized value contains a string between curly braces.
|
||||
*/
|
||||
baseText(
|
||||
key: string, options?: { interpolate: { [key: string]: string } },
|
||||
): string {
|
||||
return this.i18n.t(key, options && options.interpolate).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a string of dynamic text, i.e. a string with a constructed path to the localized value in the node text object, in the credentials modal, in the node view, or in the headers. Unlike in `baseText`, the fallback has to be set manually for dynamic text.
|
||||
*/
|
||||
private dynamicRender(
|
||||
{ key, fallback }: { key: string; fallback: string; },
|
||||
) {
|
||||
return this.i18n.te(key) ? this.i18n.t(key).toString() : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a string of dynamic header text, used in the nodes panel and in the node view.
|
||||
*/
|
||||
headerText(arg: { key: string; fallback: string; }) {
|
||||
return this.dynamicRender(arg);
|
||||
}
|
||||
|
||||
credText () {
|
||||
const { credentialTextRenderKeys: keys } = this.$store.getters;
|
||||
const nodeType = keys ? keys.nodeType : '';
|
||||
const credentialType = keys ? keys.credentialType : '';
|
||||
const credentialPrefix = `${nodeType}.${CREDENTIALS_MODAL_KEY}.${credentialType}`;
|
||||
const context = this;
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Display name for a top-level parameter in the credentials modal.
|
||||
*/
|
||||
topParameterDisplayName(
|
||||
{ name: parameterName, displayName }: { name: string; displayName: string; },
|
||||
) {
|
||||
if (['clientId', 'clientSecret'].includes(parameterName)) {
|
||||
return context.dynamicRender({
|
||||
key: `${REUSABLE_DYNAMIC_TEXT_KEY}.oauth2.${parameterName}`,
|
||||
fallback: displayName,
|
||||
});
|
||||
}
|
||||
|
||||
return context.dynamicRender({
|
||||
key: `${credentialPrefix}.${parameterName}.displayName`,
|
||||
fallback: displayName,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Description for a top-level parameter in the credentials modal.
|
||||
*/
|
||||
topParameterDescription(
|
||||
{ name: parameterName, description }: { name: string; description: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${credentialPrefix}.${parameterName}.description`,
|
||||
fallback: description,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Display name for an option inside an `options` or `multiOptions` parameter in the credentials modal.
|
||||
*/
|
||||
optionsOptionDisplayName(
|
||||
{ name: parameterName }: { name: string; },
|
||||
{ value: optionName, name: displayName }: { value: string; name: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${credentialPrefix}.${parameterName}.options.${optionName}.displayName`,
|
||||
fallback: displayName,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Description for an option inside an `options` or `multiOptions` parameter in the credentials modal.
|
||||
*/
|
||||
optionsOptionDescription(
|
||||
{ name: parameterName }: { name: string; },
|
||||
{ value: optionName, description }: { value: string; description: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${credentialPrefix}.${parameterName}.options.${optionName}.description`,
|
||||
fallback: description,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Placeholder for a `string` or `collection` or `fixedCollection` parameter in the credentials modal.
|
||||
* - For a `string` parameter, the placeholder is unselectable greyed-out sample text.
|
||||
* - For a `collection` or `fixedCollection` parameter, the placeholder is the button text.
|
||||
*/
|
||||
placeholder(
|
||||
{ name: parameterName, displayName }: { name: string; displayName: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${credentialPrefix}.${parameterName}.placeholder`,
|
||||
fallback: displayName,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
nodeText () {
|
||||
const type = this.$store.getters.activeNode.type;
|
||||
const nodePrefix = `${type}.${NODE_VIEW_KEY}`;
|
||||
const context = this;
|
||||
|
||||
return {
|
||||
/**
|
||||
* Display name for a top-level parameter in the node view.
|
||||
*/
|
||||
topParameterDisplayName(
|
||||
{ name: parameterName, displayName }: { name: string; displayName: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.displayName`,
|
||||
fallback: displayName,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Description for a top-level parameter in the node view in the node view.
|
||||
*/
|
||||
topParameterDescription(
|
||||
{ name: parameterName, description }: { name: string; description: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.description`,
|
||||
fallback: description,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Display name for an option inside a `collection` or `fixedCollection` parameter in the node view.
|
||||
*/
|
||||
collectionOptionDisplayName(
|
||||
{ name: parameterName }: { name: string; },
|
||||
{ name: optionName, displayName }: { name: string; displayName: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.options.${optionName}.displayName`,
|
||||
fallback: displayName,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Display name for an option inside an `options` or `multiOptions` parameter in the node view.
|
||||
*/
|
||||
optionsOptionDisplayName(
|
||||
{ name: parameterName }: { name: string; },
|
||||
{ value: optionName, name: displayName }: { value: string; name: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.options.${optionName}.displayName`,
|
||||
fallback: displayName,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Description for an option inside an `options` or `multiOptions` parameter in the node view.
|
||||
*/
|
||||
optionsOptionDescription(
|
||||
{ name: parameterName }: { name: string; },
|
||||
{ value: optionName, description }: { value: string; description: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.options.${optionName}.description`,
|
||||
fallback: description,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Text for a button to add another option inside a `collection` or `fixedCollection` parameter having`multipleValues: true` in the node view.
|
||||
*/
|
||||
multipleValueButtonText(
|
||||
{ name: parameterName, typeOptions: { multipleValueButtonText } }:
|
||||
{ name: string; typeOptions: { multipleValueButtonText: string; } },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.multipleValueButtonText`,
|
||||
fallback: multipleValueButtonText,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Placeholder for a `string` or `collection` or `fixedCollection` parameter in the node view.
|
||||
* - For a `string` parameter, the placeholder is unselectable greyed-out sample text.
|
||||
* - For a `collection` or `fixedCollection` parameter, the placeholder is the button text.
|
||||
*/
|
||||
placeholder(
|
||||
{ name: parameterName, placeholder }: { name: string; placeholder: string; },
|
||||
) {
|
||||
return context.dynamicRender({
|
||||
key: `${nodePrefix}.${parameterName}.placeholder`,
|
||||
fallback: placeholder,
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const i18nInstance = new VueI18n({
|
||||
locale: 'en',
|
||||
fallbackLocale: 'en',
|
||||
messages: { en: englishBaseText },
|
||||
silentTranslationWarn: true,
|
||||
});
|
||||
|
||||
const loadedLanguages = ['en'];
|
||||
|
||||
function setLanguage(language: string) {
|
||||
i18nInstance.locale = language;
|
||||
axios.defaults.headers.common['Accept-Language'] = language;
|
||||
document!.querySelector('html')!.setAttribute('lang', language);
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
export async function loadLanguage(language?: string) {
|
||||
if (!language) return Promise.resolve();
|
||||
|
||||
if (i18nInstance.locale === language) {
|
||||
return Promise.resolve(setLanguage(language));
|
||||
}
|
||||
|
||||
if (loadedLanguages.includes(language)) {
|
||||
return Promise.resolve(setLanguage(language));
|
||||
}
|
||||
|
||||
const { numberFormats, ...rest } = require(`./locales/${language}.json`);
|
||||
|
||||
i18nInstance.setLocaleMessage(language, rest);
|
||||
|
||||
if (numberFormats) {
|
||||
i18nInstance.setNumberFormat(language, numberFormats);
|
||||
}
|
||||
|
||||
loadedLanguages.push(language);
|
||||
|
||||
setLanguage(language);
|
||||
}
|
||||
|
||||
export function addNodeTranslation(
|
||||
nodeTranslation: { [key: string]: object },
|
||||
language: string,
|
||||
) {
|
||||
const newNodesBase = {
|
||||
'n8n-nodes-base': Object.assign(
|
||||
i18nInstance.messages[language]['n8n-nodes-base'] || {},
|
||||
nodeTranslation,
|
||||
),
|
||||
};
|
||||
|
||||
i18nInstance.setLocaleMessage(
|
||||
language,
|
||||
Object.assign(i18nInstance.messages[language], newNodesBase),
|
||||
);
|
||||
}
|
||||
|
||||
export function addHeaders(
|
||||
headers: INodeTranslationHeaders,
|
||||
language: string,
|
||||
) {
|
||||
i18nInstance.setLocaleMessage(
|
||||
language,
|
||||
Object.assign(i18nInstance.messages[language], { headers }),
|
||||
);
|
||||
}
|
1040
packages/editor-ui/src/plugins/i18n/locales/en.json
Normal file
7
packages/editor-ui/src/plugins/i18n/types.d.ts
vendored
Normal file
|
@ -0,0 +1,7 @@
|
|||
import { i18nClass } from '.';
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$locale: I18nClass;
|
||||
}
|
||||
}
|
|
@ -34,7 +34,7 @@ interface IUserNodesPanelSession {
|
|||
}
|
||||
|
||||
class Telemetry {
|
||||
|
||||
|
||||
private get telemetry() {
|
||||
// @ts-ignore
|
||||
return window.rudderanalytics;
|
||||
|
|
|
@ -48,6 +48,8 @@ const state: IRootState = {
|
|||
activeNode: null,
|
||||
// @ts-ignore
|
||||
baseUrl: process.env.VUE_APP_URL_BASE_API ? process.env.VUE_APP_URL_BASE_API : (window.BASE_PATH === '/%BASE_PATH%/' ? '/' : window.BASE_PATH),
|
||||
credentialTextRenderKeys: null,
|
||||
defaultLocale: 'en',
|
||||
endpointWebhook: 'webhook',
|
||||
endpointWebhookTest: 'webhook-test',
|
||||
executionId: null,
|
||||
|
@ -552,9 +554,15 @@ export const store = new Vuex.Store({
|
|||
setN8nMetadata(state, metadata: IDataObject) {
|
||||
Vue.set(state, 'n8nMetadata', metadata);
|
||||
},
|
||||
setDefaultLocale(state, locale: string) {
|
||||
Vue.set(state, 'defaultLocale', locale);
|
||||
},
|
||||
setActiveNode (state, nodeName: string) {
|
||||
state.activeNode = nodeName;
|
||||
},
|
||||
setCredentialTextRenderKeys (state, renderKeys: { nodeType: string; credentialType: string; }) {
|
||||
state.credentialTextRenderKeys = renderKeys;
|
||||
},
|
||||
|
||||
setLastSelectedNode (state, nodeName: string) {
|
||||
state.lastSelectedNode = nodeName;
|
||||
|
@ -660,6 +668,10 @@ export const store = new Vuex.Store({
|
|||
return state.activeExecutions;
|
||||
},
|
||||
|
||||
credentialTextRenderKeys: (state): object | null => {
|
||||
return state.credentialTextRenderKeys;
|
||||
},
|
||||
|
||||
getBaseUrl: (state): string => {
|
||||
return state.baseUrl;
|
||||
},
|
||||
|
@ -728,6 +740,9 @@ export const store = new Vuex.Store({
|
|||
n8nMetadata: (state): object => {
|
||||
return state.n8nMetadata;
|
||||
},
|
||||
defaultLocale: (state): string => {
|
||||
return state.defaultLocale;
|
||||
},
|
||||
|
||||
// Push Connection
|
||||
pushConnectionActive: (state): boolean => {
|
||||
|
@ -812,6 +827,21 @@ export const store = new Vuex.Store({
|
|||
allNodeTypes: (state): INodeTypeDescription[] => {
|
||||
return state.nodeTypes;
|
||||
},
|
||||
|
||||
/**
|
||||
* Getter for node default names ending with a number: `'S3'`, `'Magento 2'`, etc.
|
||||
*/
|
||||
nativelyNumberSuffixedDefaults: (_, getters): string[] => {
|
||||
const { allNodeTypes } = getters as {
|
||||
allNodeTypes: Array<INodeTypeDescription & { defaults: { name: string } }>;
|
||||
};
|
||||
|
||||
return allNodeTypes.reduce<string[]>((acc, cur) => {
|
||||
if (/\d$/.test(cur.defaults.name)) acc.push(cur.defaults.name);
|
||||
return acc;
|
||||
}, []);
|
||||
},
|
||||
|
||||
nodeType: (state, getters) => (nodeType: string, typeVersion?: number): INodeTypeDescription | null => {
|
||||
const foundType = state.nodeTypes.find(typeData => {
|
||||
return typeData.name === nodeType && typeData.version === (typeVersion || typeData.defaultVersion || DEFAULT_NODETYPE_VERSION);
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<DataDisplay @valueChanged="valueChanged"/>
|
||||
<div v-if="!createNodeActive && !isReadOnly" class="node-creator-button" title="Add Node" @click="() => openNodeCreator('add_node_button')">
|
||||
<div v-if="!createNodeActive && !isReadOnly" class="node-creator-button" :title="$locale.baseText('nodeView.addNode')" @click="() => openNodeCreator('add_node_button')">
|
||||
<n8n-icon-button size="xlarge" icon="plus" />
|
||||
</div>
|
||||
<node-creator
|
||||
|
@ -43,22 +43,22 @@
|
|||
@closeNodeCreator="closeNodeCreator"
|
||||
></node-creator>
|
||||
<div :class="{ 'zoom-menu': true, expanded: !sidebarMenuCollapsed }">
|
||||
<button @click="zoomToFit" class="button-white" title="Zoom to Fit">
|
||||
<button @click="zoomToFit" class="button-white" :title="$locale.baseText('nodeView.zoomToFit')">
|
||||
<font-awesome-icon icon="expand"/>
|
||||
</button>
|
||||
<button @click="zoomIn()" class="button-white" title="Zoom In">
|
||||
<button @click="zoomIn()" class="button-white" :title="$locale.baseText('nodeView.zoomIn')">
|
||||
<font-awesome-icon icon="search-plus"/>
|
||||
</button>
|
||||
<button @click="zoomOut()" class="button-white" title="Zoom Out">
|
||||
<button @click="zoomOut()" class="button-white" :title="$locale.baseText('nodeView.zoomOut')">
|
||||
<font-awesome-icon icon="search-minus"/>
|
||||
</button>
|
||||
<button
|
||||
v-if="nodeViewScale !== 1"
|
||||
@click="resetZoom()"
|
||||
class="button-white"
|
||||
title="Reset Zoom"
|
||||
:title="$locale.baseText('nodeView.resetZoom')"
|
||||
>
|
||||
<font-awesome-icon icon="undo" title="Reset Zoom"/>
|
||||
<font-awesome-icon icon="undo" :title="$locale.baseText('nodeView.resetZoom')"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="workflow-execute-wrapper" v-if="!isReadOnly">
|
||||
|
@ -68,7 +68,7 @@
|
|||
:label="runButtonText"
|
||||
size="large"
|
||||
icon="play-circle"
|
||||
title="Executes the Workflow from the Start or Webhook Node."
|
||||
:title="$locale.baseText('nodeView.executesTheWorkflowFromTheStartOrWebhookNode')"
|
||||
:type="workflowRunning ? 'light' : 'primary'"
|
||||
/>
|
||||
|
||||
|
@ -78,7 +78,10 @@
|
|||
size="large"
|
||||
class="stop-execution"
|
||||
type="light"
|
||||
:title="stopExecutionInProgress ? 'Stopping current execution':'Stop current execution'"
|
||||
:title="stopExecutionInProgress
|
||||
? $locale.baseText('nodeView.stoppingCurrentExecution')
|
||||
: $locale.baseText('nodeView.stopCurrentExecution')
|
||||
"
|
||||
:loading="stopExecutionInProgress"
|
||||
@click.stop="stopExecution()"
|
||||
/>
|
||||
|
@ -88,14 +91,14 @@
|
|||
class="stop-execution"
|
||||
icon="stop"
|
||||
size="large"
|
||||
title="Stop waiting for Webhook call"
|
||||
:title="$locale.baseText('nodeView.stopWaitingForWebhookCall')"
|
||||
type="light"
|
||||
@click.stop="stopWaitingForWebhook()"
|
||||
/>
|
||||
|
||||
<n8n-icon-button
|
||||
v-if="!isReadOnly && workflowExecution && !workflowRunning"
|
||||
title="Deletes the current Execution Data."
|
||||
:title="$locale.baseText('nodeView.deletesTheCurrentExecutionData')"
|
||||
icon="trash"
|
||||
size="large"
|
||||
@click.stop="clearExecutionData()"
|
||||
|
@ -167,6 +170,13 @@ import {
|
|||
IExecutionsSummary,
|
||||
} from '../Interface';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
import {
|
||||
loadLanguage,
|
||||
addNodeTranslation,
|
||||
addHeaders,
|
||||
} from '@/plugins/i18n';
|
||||
|
||||
import '../plugins/N8nCustomConnectorType';
|
||||
import '../plugins/PlusEndpointType';
|
||||
|
||||
|
@ -224,11 +234,21 @@ export default mixins(
|
|||
},
|
||||
deep: true,
|
||||
},
|
||||
|
||||
async defaultLocale (newLocale, oldLocale) {
|
||||
loadLanguage(newLocale);
|
||||
},
|
||||
},
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.message'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.confirmButtonText'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.beforeRouteLeave.cancelButtonText'),
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
next(false);
|
||||
} else {
|
||||
|
@ -244,6 +264,13 @@ export default mixins(
|
|||
...mapGetters('ui', [
|
||||
'sidebarMenuCollapsed',
|
||||
]),
|
||||
defaultLocale (): string {
|
||||
return this.$store.getters.defaultLocale;
|
||||
},
|
||||
englishLocale(): boolean {
|
||||
return this.defaultLocale === 'en';
|
||||
},
|
||||
...mapGetters(['nativelyNumberSuffixedDefaults']),
|
||||
activeNode (): INodeUi | null {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
|
@ -258,14 +285,14 @@ export default mixins(
|
|||
},
|
||||
runButtonText (): string {
|
||||
if (this.workflowRunning === false) {
|
||||
return 'Execute Workflow';
|
||||
return this.$locale.baseText('nodeView.runButtonText.executeWorkflow');
|
||||
}
|
||||
|
||||
if (this.executionWaitingForWebhook === true) {
|
||||
return 'Waiting for Trigger Event';
|
||||
return this.$locale.baseText('nodeView.runButtonText.waitingForTriggerEvent');
|
||||
}
|
||||
|
||||
return 'Executing Workflow';
|
||||
return this.$locale.baseText('nodeView.runButtonText.executingWorkflow');
|
||||
},
|
||||
workflowStyle (): object {
|
||||
const offsetPosition = this.$store.getters.getNodeViewOffsetPosition;
|
||||
|
@ -327,6 +354,76 @@ export default mixins(
|
|||
this.$store.commit('setWorkflowExecutionData', null);
|
||||
this.updateNodesExecutionIssues();
|
||||
},
|
||||
translateName(type: string, originalName: string) {
|
||||
return this.$locale.headerText({
|
||||
key: `headers.${this.$locale.shortNodeType(type)}.displayName`,
|
||||
fallback: originalName,
|
||||
});
|
||||
},
|
||||
getUniqueNodeName({
|
||||
originalName,
|
||||
additionalUsedNames = [],
|
||||
type = '',
|
||||
} : {
|
||||
originalName: string,
|
||||
additionalUsedNames?: string[],
|
||||
type?: string,
|
||||
}) {
|
||||
const allNodeNamesOnCanvas = this.$store.getters.allNodes.map((n: INodeUi) => n.name);
|
||||
originalName = this.englishLocale ? originalName : this.translateName(type, originalName);
|
||||
|
||||
if (
|
||||
!allNodeNamesOnCanvas.includes(originalName) &&
|
||||
!additionalUsedNames.includes(originalName)
|
||||
) {
|
||||
return originalName; // already unique
|
||||
}
|
||||
|
||||
let natives: string[] = this.nativelyNumberSuffixedDefaults;
|
||||
natives = this.englishLocale ? natives : natives.map(name => {
|
||||
const type = name.toLowerCase().replace('_', '');
|
||||
return this.translateName(type, name);
|
||||
});
|
||||
|
||||
const found = natives.find((n) => originalName.startsWith(n));
|
||||
|
||||
let ignore, baseName, nameIndex, uniqueName;
|
||||
let index = 1;
|
||||
|
||||
if (found) {
|
||||
// name natively ends with number
|
||||
nameIndex = originalName.split(found).pop();
|
||||
if (nameIndex) {
|
||||
index = parseInt(nameIndex, 10);
|
||||
}
|
||||
baseName = uniqueName = originalName;
|
||||
} else {
|
||||
const nameMatch = originalName.match(/(.*\D+)(\d*)/);
|
||||
|
||||
if (nameMatch === null) {
|
||||
// name is only a number
|
||||
index = parseInt(originalName, 10);
|
||||
baseName = '';
|
||||
uniqueName = baseName + index;
|
||||
} else {
|
||||
// name is string or string/number combination
|
||||
[ignore, baseName, nameIndex] = nameMatch;
|
||||
if (nameIndex !== '') {
|
||||
index = parseInt(nameIndex, 10);
|
||||
}
|
||||
uniqueName = baseName = originalName;
|
||||
}
|
||||
}
|
||||
|
||||
while (
|
||||
allNodeNamesOnCanvas.includes(uniqueName) ||
|
||||
additionalUsedNames.includes(uniqueName)
|
||||
) {
|
||||
uniqueName = baseName + (index++);
|
||||
}
|
||||
|
||||
return uniqueName;
|
||||
},
|
||||
async onSaveKeyboardShortcut () {
|
||||
const saved = await this.saveCurrentWorkflow();
|
||||
if (saved) this.$store.dispatch('settings/fetchPromptsData');
|
||||
|
@ -343,7 +440,11 @@ export default mixins(
|
|||
try {
|
||||
data = await this.restApi().getExecution(executionId);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem loading execution', 'There was a problem opening the execution:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.openExecution.title'),
|
||||
this.$locale.baseText('nodeView.showError.openExecution.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -402,15 +503,15 @@ export default mixins(
|
|||
|
||||
if ((data as IExecutionsSummary).waitTill) {
|
||||
this.$showMessage({
|
||||
title: `This execution hasn't finished yet`,
|
||||
message: `<a onclick="window.location.reload(false);">Refresh</a> to see the latest status.<br/> <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait/" target="_blank">More info</a>`,
|
||||
title: this.$locale.baseText('nodeView.thisExecutionHasntFinishedYet'),
|
||||
message: `<a onclick="window.location.reload(false);">${this.$locale.baseText('nodeView.refresh')}</a> ${this.$locale.baseText('nodeView.toSeeTheLatestStatus')}.<br/> <a href="https://docs.n8n.io/nodes/n8n-nodes-base.wait/" target="_blank">${this.$locale.baseText('nodeView.moreInfo')}</a>`,
|
||||
type: 'warning',
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
},
|
||||
async openWorkflowTemplate (templateId: string) {
|
||||
this.setLoadingText('Loading template');
|
||||
this.setLoadingText(this.$locale.baseText('nodeView.loadingTemplate'));
|
||||
this.resetWorkspace();
|
||||
|
||||
let data: IWorkflowTemplate | undefined;
|
||||
|
@ -419,17 +520,21 @@ export default mixins(
|
|||
data = await this.$store.dispatch('workflows/getWorkflowTemplate', templateId);
|
||||
|
||||
if (!data) {
|
||||
throw new Error(`Workflow template with id "${templateId}" could not be found!`);
|
||||
throw new Error(
|
||||
this.$locale.baseText(
|
||||
'nodeView.workflowTemplateWithIdCouldNotBeFound',
|
||||
{ interpolate: { templateId } },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
data.workflow.nodes.forEach((node) => {
|
||||
if (!this.$store.getters.nodeType(node.type)) {
|
||||
const name = node.type.replace('n8n-nodes-base.', '');
|
||||
throw new Error(`The ${name} node is not supported`);
|
||||
throw new Error(`The ${this.$locale.shortNodeType(node.type)} node is not supported`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(error, `Couldn't import workflow`);
|
||||
this.$showError(error, this.$locale.baseText('nodeView.couldntImportWorkflow'));
|
||||
this.$router.push({ name: 'NodeViewNew' });
|
||||
return;
|
||||
}
|
||||
|
@ -470,12 +575,21 @@ export default mixins(
|
|||
try {
|
||||
data = await this.restApi().getWorkflow(workflowId);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem opening workflow', 'There was a problem opening the workflow:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.openWorkflow.title'),
|
||||
this.$locale.baseText('nodeView.showError.openWorkflow.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
throw new Error(`Workflow with id "${workflowId}" could not be found!`);
|
||||
throw new Error(
|
||||
this.$locale.baseText(
|
||||
'nodeView.workflowWithIdCouldNotBeFound',
|
||||
{ interpolate: { workflowId } },
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
this.$store.commit('setActive', data.active || false);
|
||||
|
@ -633,8 +747,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: 'Workflow created',
|
||||
message: 'A new workflow was successfully created!',
|
||||
title: this.$locale.baseText('nodeView.showMessage.keyDown.title'),
|
||||
message: this.$locale.baseText('nodeView.showMessage.keyDown.message'),
|
||||
type: 'success',
|
||||
});
|
||||
} else if ((e.key === 's') && (this.isCtrlKeyPressed(e) === true)) {
|
||||
|
@ -931,8 +1045,11 @@ export default mixins(
|
|||
this.stopExecutionInProgress = true;
|
||||
await this.restApi().stopCurrentExecution(executionId);
|
||||
this.$showMessage({
|
||||
title: 'Execution stopped',
|
||||
message: `The execution with the id "${executionId}" was stopped!`,
|
||||
title: this.$locale.baseText('nodeView.showMessage.stopExecutionTry.title'),
|
||||
message: this.$locale.baseText(
|
||||
'nodeView.showMessage.stopExecutionTry.message',
|
||||
{ interpolate: { executionId } },
|
||||
),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -957,12 +1074,16 @@ export default mixins(
|
|||
this.$store.commit('setWorkflowExecutionData', executedData);
|
||||
this.$store.commit('removeActiveAction', 'workflowRunning');
|
||||
this.$showMessage({
|
||||
title: 'Workflow finished executing',
|
||||
message: 'Unable to stop operation in time. Workflow finished executing already.',
|
||||
title: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.title'),
|
||||
message: this.$locale.baseText('nodeView.showMessage.stopExecutionCatch.message'),
|
||||
type: 'success',
|
||||
});
|
||||
} else {
|
||||
this.$showError(error, 'Problem stopping execution', 'There was a problem stopping the execuction:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.stopExecution.title'),
|
||||
this.$locale.baseText('nodeView.showError.stopExecution.message') + ':',
|
||||
);
|
||||
}
|
||||
}
|
||||
this.stopExecutionInProgress = false;
|
||||
|
@ -972,9 +1093,19 @@ export default mixins(
|
|||
try {
|
||||
await this.restApi().removeTestWebhook(this.$store.getters.workflowId);
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem deleting the test-webhook', 'There was a problem deleting webhook:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.stopWaitingForWebhook.title'),
|
||||
this.$locale.baseText('nodeView.showError.stopWaitingForWebhook.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.$showMessage({
|
||||
title: this.$locale.baseText('nodeView.showMessage.stopWaitingForWebhook.title'),
|
||||
message: this.$locale.baseText('nodeView.showMessage.stopWaitingForWebhook.message'),
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -991,7 +1122,16 @@ export default mixins(
|
|||
return;
|
||||
}
|
||||
|
||||
const importConfirm = await this.confirmMessage(`Import workflow from this URL:<br /><i>${plainTextData}<i>`, 'Import Workflow from URL?', 'warning', 'Yes, import!');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText(
|
||||
'nodeView.confirmMessage.receivedCopyPasteData.message',
|
||||
{ interpolate: { plainTextData } },
|
||||
),
|
||||
this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.confirmButtonText'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.receivedCopyPasteData.cancelButtonText'),
|
||||
);
|
||||
|
||||
if (importConfirm === false) {
|
||||
return;
|
||||
|
@ -1033,7 +1173,11 @@ export default mixins(
|
|||
workflowData = await this.restApi().getWorkflowFromUrl(url);
|
||||
} catch (error) {
|
||||
this.stopLoading();
|
||||
this.$showError(error, 'Problem loading workflow', 'There was a problem loading the workflow data from URL:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.getWorkflowDataFromUrl.title'),
|
||||
this.$locale.baseText('nodeView.showError.getWorkflowDataFromUrl.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
this.stopLoading();
|
||||
|
@ -1071,7 +1215,11 @@ export default mixins(
|
|||
});
|
||||
});
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Problem importing workflow', 'There was a problem importing workflow data:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.importWorkflowData.title'),
|
||||
this.$locale.baseText('nodeView.showError.importWorkflowData.message') + ':',
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1113,8 +1261,18 @@ export default mixins(
|
|||
showMaxNodeTypeError (nodeTypeData: INodeTypeDescription) {
|
||||
const maxNodes = nodeTypeData.maxNodes;
|
||||
this.$showMessage({
|
||||
title: 'Could not create node!',
|
||||
message: `Node can not be created because in a workflow max. ${maxNodes} ${maxNodes === 1 ? 'node' : 'nodes'} of type "${nodeTypeData.displayName}" ${maxNodes === 1 ? 'is' : 'are'} allowed!`,
|
||||
title: this.$locale.baseText('nodeView.showMessage.showMaxNodeTypeError.title'),
|
||||
message: this.$locale.baseText(
|
||||
maxNodes === 1
|
||||
? 'nodeView.showMessage.showMaxNodeTypeError.message.singular'
|
||||
: 'nodeView.showMessage.showMaxNodeTypeError.message.plural',
|
||||
{
|
||||
interpolate: {
|
||||
maxNodes: maxNodes!.toString(),
|
||||
nodeTypeDataDisplayName: nodeTypeData.displayName,
|
||||
},
|
||||
},
|
||||
),
|
||||
type: 'error',
|
||||
duration: 0,
|
||||
});
|
||||
|
@ -1124,8 +1282,11 @@ export default mixins(
|
|||
|
||||
if (nodeTypeData === null) {
|
||||
this.$showMessage({
|
||||
title: 'Could not create node!',
|
||||
message: `Node of type "${nodeTypeName}" could not be created as it is not known.`,
|
||||
title: this.$locale.baseText('nodeView.showMessage.addNodeButton.title'),
|
||||
message: this.$locale.baseText(
|
||||
'nodeView.showMessage.addNodeButton.message',
|
||||
{ interpolate: { nodeTypeName } },
|
||||
),
|
||||
type: 'error',
|
||||
});
|
||||
return;
|
||||
|
@ -1186,8 +1347,12 @@ export default mixins(
|
|||
newNodeData.position = CanvasHelpers.getNewNodePosition(this.nodes, this.lastClickPosition);
|
||||
}
|
||||
|
||||
|
||||
// Check if node-name is unique else find one that is
|
||||
newNodeData.name = CanvasHelpers.getUniqueNodeName(this.$store.getters.allNodes, newNodeData.name);
|
||||
newNodeData.name = this.getUniqueNodeName({
|
||||
originalName: newNodeData.name,
|
||||
type: newNodeData.type,
|
||||
});
|
||||
|
||||
if (nodeTypeData.webhooks && nodeTypeData.webhooks.length) {
|
||||
newNodeData.webhookId = uuidv4();
|
||||
|
@ -1526,6 +1691,7 @@ export default mixins(
|
|||
this.pullConnActive = true;
|
||||
this.newNodeInsertPosition = null;
|
||||
CanvasHelpers.resetConnection(connection);
|
||||
|
||||
const nodes = [...document.querySelectorAll('.node-default')];
|
||||
|
||||
const onMouseMove = (e: MouseEvent | TouchEvent) => {
|
||||
|
@ -1636,7 +1802,13 @@ export default mixins(
|
|||
|
||||
const result = this.$store.getters.getStateIsDirty;
|
||||
if(result) {
|
||||
const importConfirm = await this.confirmMessage(`When you switch workflows your current workflow changes will be lost.`, 'Save your Changes?', 'warning', 'Yes, switch workflows and forget changes');
|
||||
const importConfirm = await this.confirmMessage(
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.message'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.headline'),
|
||||
'warning',
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.confirmButtonText'),
|
||||
this.$locale.baseText('nodeView.confirmMessage.initView.cancelButtonText'),
|
||||
);
|
||||
if (importConfirm === false) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
@ -1674,12 +1846,13 @@ export default mixins(
|
|||
|
||||
window.addEventListener("beforeunload", (e) => {
|
||||
if(this.$store.getters.getStateIsDirty === true) {
|
||||
const confirmationMessage = 'It looks like you have been editing something. '
|
||||
+ 'If you leave before saving, your changes will be lost.';
|
||||
const confirmationMessage = this.$locale.baseText('nodeView.itLooksLikeYouHaveBeenEditingSomething');
|
||||
(e || window.event).returnValue = confirmationMessage; //Gecko + IE
|
||||
return confirmationMessage; //Gecko + Webkit, Safari, Chrome etc.
|
||||
} else {
|
||||
this.startLoading('Redirecting');
|
||||
this.startLoading(
|
||||
this.$locale.baseText('nodeView.redirecting'),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
@ -1783,7 +1956,10 @@ export default mixins(
|
|||
const newNodeData = JSON.parse(JSON.stringify(this.getNodeDataToSave(node)));
|
||||
|
||||
// Check if node-name is unique else find one that is
|
||||
newNodeData.name = CanvasHelpers.getUniqueNodeName(this.$store.getters.allNodes, newNodeData.name);
|
||||
newNodeData.name = this.getUniqueNodeName({
|
||||
originalName: newNodeData.name,
|
||||
type: newNodeData.type,
|
||||
});
|
||||
|
||||
newNodeData.position = CanvasHelpers.getNewNodePosition(
|
||||
this.nodes,
|
||||
|
@ -2028,13 +2204,17 @@ export default mixins(
|
|||
},
|
||||
async renameNodePrompt (currentName: string) {
|
||||
try {
|
||||
const promptResponsePromise = this.$prompt('New Name:', `Rename Node: "${currentName}"`, {
|
||||
customClass: 'rename-prompt',
|
||||
confirmButtonText: 'Rename',
|
||||
cancelButtonText: 'Cancel',
|
||||
inputErrorMessage: 'Invalid Name',
|
||||
inputValue: currentName,
|
||||
});
|
||||
const promptResponsePromise = this.$prompt(
|
||||
this.$locale.baseText('nodeView.prompt.newName') + ':',
|
||||
this.$locale.baseText('nodeView.prompt.renameNode') + `: ${currentName}`,
|
||||
{
|
||||
customClass: 'rename-prompt',
|
||||
confirmButtonText: this.$locale.baseText('nodeView.prompt.rename'),
|
||||
cancelButtonText: this.$locale.baseText('nodeView.prompt.cancel'),
|
||||
inputErrorMessage: this.$locale.baseText('nodeView.prompt.invalidName'),
|
||||
inputValue: currentName,
|
||||
},
|
||||
);
|
||||
|
||||
// Wait till it had time to display
|
||||
await Vue.nextTick();
|
||||
|
@ -2056,7 +2236,9 @@ export default mixins(
|
|||
return;
|
||||
}
|
||||
// Check if node-name is unique else find one that is
|
||||
newName = CanvasHelpers.getUniqueNodeName(this.$store.getters.allNodes, newName);
|
||||
newName = this.getUniqueNodeName({
|
||||
originalName: newName,
|
||||
});
|
||||
|
||||
// Rename the node and update the connections
|
||||
const workflow = this.getWorkflow(undefined, undefined, true);
|
||||
|
@ -2167,7 +2349,7 @@ export default mixins(
|
|||
try {
|
||||
nodeParameters = NodeHelpers.getNodeParameters(nodeType.properties, node.parameters, true, false);
|
||||
} catch (e) {
|
||||
console.error(`There was a problem loading the node-parameters of node: "${node.name}"`); // eslint-disable-line no-console
|
||||
console.error(this.$locale.baseText('nodeView.thereWasAProblemLoadingTheNodeParametersOfNode') + `: "${node.name}"`); // eslint-disable-line no-console
|
||||
console.error(e); // eslint-disable-line no-console
|
||||
}
|
||||
node.parameters = nodeParameters !== null ? nodeParameters : {};
|
||||
|
@ -2242,7 +2424,9 @@ export default mixins(
|
|||
|
||||
if (!data.nodes) {
|
||||
// No nodes to add
|
||||
throw new Error('No nodes given to add!');
|
||||
throw new Error(
|
||||
this.$locale.baseText('nodeView.noNodesGivenToAdd'),
|
||||
);
|
||||
}
|
||||
|
||||
// Get how many of the nodes of the types which have
|
||||
|
@ -2273,7 +2457,11 @@ export default mixins(
|
|||
}
|
||||
|
||||
oldName = node.name;
|
||||
newName = CanvasHelpers.getUniqueNodeName(this.$store.getters.allNodes, node.name, newNodeNames);
|
||||
newName = this.getUniqueNodeName({
|
||||
originalName: node.name,
|
||||
additionalUsedNames: newNodeNames,
|
||||
type: node.type,
|
||||
});
|
||||
|
||||
newNodeNames.push(newName);
|
||||
nodeNameTable[oldName] = newName;
|
||||
|
@ -2477,8 +2665,16 @@ export default mixins(
|
|||
if (nodesToBeFetched.length > 0) {
|
||||
// Only call API if node information is actually missing
|
||||
this.startLoading();
|
||||
const nodeInfo = await this.restApi().getNodesInformation(nodesToBeFetched);
|
||||
this.$store.commit('updateNodeTypes', nodeInfo);
|
||||
|
||||
const nodesInfo = await this.restApi().getNodesInformation(nodesToBeFetched);
|
||||
|
||||
nodesInfo.forEach(nodeInfo => {
|
||||
if (nodeInfo.translation) {
|
||||
addNodeTranslation(nodeInfo.translation, this.$store.getters.defaultLocale);
|
||||
}
|
||||
});
|
||||
|
||||
this.$store.commit('updateNodeTypes', nodesInfo);
|
||||
this.stopLoading();
|
||||
}
|
||||
},
|
||||
|
@ -2511,8 +2707,21 @@ export default mixins(
|
|||
|
||||
try {
|
||||
await Promise.all(loadPromises);
|
||||
|
||||
if (this.defaultLocale !== 'en') {
|
||||
try {
|
||||
const headers = await this.restApi().getNodeTranslationHeaders();
|
||||
addHeaders(headers, this.defaultLocale);
|
||||
} catch (_) {
|
||||
// no headers available
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Init Problem', 'There was a problem loading init data:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.mounted1.title'),
|
||||
this.$locale.baseText('nodeView.showError.mounted1.message') + ':',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -2521,7 +2730,11 @@ export default mixins(
|
|||
this.initNodeView();
|
||||
await this.initView();
|
||||
} catch (error) {
|
||||
this.$showError(error, 'Init Problem', 'There was a problem initializing the workflow:');
|
||||
this.$showError(
|
||||
error,
|
||||
this.$locale.baseText('nodeView.showError.mounted2.title'),
|
||||
this.$locale.baseText('nodeView.showError.mounted2.message') + ':',
|
||||
);
|
||||
}
|
||||
this.stopLoading();
|
||||
|
||||
|
|
|
@ -613,48 +613,6 @@ export const getZoomToFit = (nodes: INodeUi[]): {offset: XYPosition, zoomLevel:
|
|||
};
|
||||
};
|
||||
|
||||
export const getUniqueNodeName = (nodes: INodeUi[], originalName: string, additinalUsedNames?: string[]) => {
|
||||
// Check if node-name is unique else find one that is
|
||||
additinalUsedNames = additinalUsedNames || [];
|
||||
|
||||
// Get all the names of the current nodes
|
||||
const nodeNames = nodes.map((node: INodeUi) => {
|
||||
return node.name;
|
||||
});
|
||||
|
||||
// Check first if the current name is already unique
|
||||
if (!nodeNames.includes(originalName) && !additinalUsedNames.includes(originalName)) {
|
||||
return originalName;
|
||||
}
|
||||
|
||||
const nameMatch = originalName.match(/(.*\D+)(\d*)/);
|
||||
let ignore, baseName, nameIndex, uniqueName;
|
||||
let index = 1;
|
||||
|
||||
if (nameMatch === null) {
|
||||
// Name is only a number
|
||||
index = parseInt(originalName, 10);
|
||||
baseName = '';
|
||||
uniqueName = baseName + index;
|
||||
} else {
|
||||
// Name is string or string/number combination
|
||||
[ignore, baseName, nameIndex] = nameMatch;
|
||||
if (nameIndex !== '') {
|
||||
index = parseInt(nameIndex, 10);
|
||||
}
|
||||
uniqueName = baseName;
|
||||
}
|
||||
|
||||
while (
|
||||
nodeNames.includes(uniqueName) ||
|
||||
additinalUsedNames.includes(uniqueName)
|
||||
) {
|
||||
uniqueName = baseName + (index++);
|
||||
}
|
||||
|
||||
return uniqueName;
|
||||
};
|
||||
|
||||
export const showDropConnectionState = (connection: Connection, targetEndpoint?: Endpoint) => {
|
||||
if (connection && connection.connector) {
|
||||
if (targetEndpoint) {
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
module.exports = {
|
||||
chainWebpack: config => config.resolve.symlinks(false),
|
||||
chainWebpack: config => {
|
||||
config.resolve.symlinks(false);
|
||||
// config.plugins.delete("prefetch"); // enable when language package grows
|
||||
},
|
||||
// transpileDependencies: [
|
||||
// // 'node_modules/quill'
|
||||
// /\/node_modules\/quill\//
|
||||
|
@ -8,6 +11,12 @@ module.exports = {
|
|||
webpackBundleAnalyzer: {
|
||||
openAnalyzer: false,
|
||||
},
|
||||
i18n: {
|
||||
locale: "en",
|
||||
fallbackLocale: "en",
|
||||
localeDir: "./src/i18n/locales",
|
||||
enableInSFC: false,
|
||||
},
|
||||
},
|
||||
configureWebpack: {
|
||||
devServer: {
|
||||
|
|
|
@ -1,11 +1,127 @@
|
|||
const { src, dest } = require('gulp');
|
||||
const { existsSync, promises: { writeFile } } = require('fs');
|
||||
const path = require('path');
|
||||
const { task, src, dest } = require('gulp');
|
||||
|
||||
const ALLOWED_HEADER_KEYS = ['displayName', 'description'];
|
||||
const PURPLE_ANSI_COLOR_CODE = 35;
|
||||
|
||||
task('build:icons', copyIcons);
|
||||
|
||||
function copyIcons() {
|
||||
src('nodes/**/*.{png,svg}')
|
||||
.pipe(dest('dist/nodes'))
|
||||
src('nodes/**/*.{png,svg}').pipe(dest('dist/nodes'))
|
||||
|
||||
return src('credentials/**/*.{png,svg}')
|
||||
.pipe(dest('dist/credentials'));
|
||||
return src('credentials/**/*.{png,svg}').pipe(dest('dist/credentials'));
|
||||
}
|
||||
|
||||
exports.default = copyIcons;
|
||||
task('build:translations', writeHeaders);
|
||||
|
||||
/**
|
||||
* Write all node translation headers at `/dist/nodes/headers.js`.
|
||||
*/
|
||||
function writeHeaders(done) {
|
||||
const { N8N_DEFAULT_LOCALE: locale } = process.env;
|
||||
|
||||
log(`Default locale set to: ${colorize(PURPLE_ANSI_COLOR_CODE, locale || 'en')}`);
|
||||
|
||||
if (!locale || locale === 'en') {
|
||||
log('No translation required - Skipping translations build...');
|
||||
return done();
|
||||
};
|
||||
|
||||
const paths = getTranslationPaths();
|
||||
const headers = getHeaders(paths);
|
||||
|
||||
const headersDestinationPath = path.join(__dirname, 'dist', 'nodes', 'headers.js');
|
||||
|
||||
writeDestinationFile(headersDestinationPath, headers);
|
||||
|
||||
log('Headers translation file written to:');
|
||||
log(headersDestinationPath, { bulletpoint: true });
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
function getTranslationPaths() {
|
||||
const destinationPaths = require('./package.json').n8n.nodes;
|
||||
const { N8N_DEFAULT_LOCALE: locale } = process.env;
|
||||
const seen = {};
|
||||
|
||||
return destinationPaths.reduce((acc, cur) => {
|
||||
const sourcePath = path.join(
|
||||
__dirname,
|
||||
cur.split('/').slice(1, -1).join('/'),
|
||||
'translations',
|
||||
`${locale}.json`,
|
||||
);
|
||||
|
||||
if (existsSync(sourcePath) && !seen[sourcePath]) {
|
||||
seen[sourcePath] = true;
|
||||
|
||||
const destinationPath = path.join(
|
||||
__dirname,
|
||||
cur.split('/').slice(0, -1).join('/'),
|
||||
'translations',
|
||||
`${locale}.json`,
|
||||
);
|
||||
|
||||
acc.push({
|
||||
source: sourcePath,
|
||||
destination: destinationPath,
|
||||
});
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function getHeaders(paths) {
|
||||
return paths.reduce((acc, cur) => {
|
||||
const translation = require(cur.source);
|
||||
const nodeTypes = Object.keys(translation);
|
||||
|
||||
for (const nodeType of nodeTypes) {
|
||||
const { header } = translation[nodeType];
|
||||
|
||||
if (isValidHeader(header, ALLOWED_HEADER_KEYS)) {
|
||||
acc[nodeType] = header;
|
||||
}
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// helpers
|
||||
// ----------------------------------
|
||||
|
||||
function isValidHeader(header, allowedHeaderKeys) {
|
||||
if (!header) return false;
|
||||
|
||||
const headerKeys = Object.keys(header);
|
||||
|
||||
return headerKeys.length > 0 &&
|
||||
headerKeys.every(key => allowedHeaderKeys.includes(key));
|
||||
}
|
||||
|
||||
function writeDestinationFile(destinationPath, data) {
|
||||
writeFile(
|
||||
destinationPath,
|
||||
`module.exports = ${JSON.stringify(data, null, 2)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const log = (string, { bulletpoint } = { bulletpoint: false }) => {
|
||||
if (bulletpoint) {
|
||||
process.stdout.write(
|
||||
colorize(PURPLE_ANSI_COLOR_CODE, `- ${string}\n`),
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
process.stdout.write(`${string}\n`);
|
||||
};
|
||||
|
||||
const colorize = (ansiColorCode, string) =>
|
||||
['\033[', ansiColorCode, 'm', string, '\033[0m'].join('');
|
||||
|
|
|
@ -16,7 +16,8 @@
|
|||
"types": "dist/src/index.d.ts",
|
||||
"scripts": {
|
||||
"dev": "npm run watch",
|
||||
"build": "tsc && gulp",
|
||||
"build": "tsc && gulp build:icons && gulp build:translations",
|
||||
"build:translations": "gulp build:translations",
|
||||
"format": "cd ../.. && node_modules/prettier/bin-prettier.js packages/nodes-base/**/**.ts --write",
|
||||
"lint": "tslint -p tsconfig.json -c tslint.json",
|
||||
"lintfix": "tslint --fix -p tsconfig.json -c tslint.json",
|
||||
|
@ -241,6 +242,7 @@
|
|||
"dist/credentials/SeaTableApi.credentials.js",
|
||||
"dist/credentials/SecurityScorecardApi.credentials.js",
|
||||
"dist/credentials/SegmentApi.credentials.js",
|
||||
"dist/credentials/SegmentApi.credentials.js",
|
||||
"dist/credentials/SendGridApi.credentials.js",
|
||||
"dist/credentials/SendyApi.credentials.js",
|
||||
"dist/credentials/SentryIoApi.credentials.js",
|
||||
|
@ -250,6 +252,7 @@
|
|||
"dist/credentials/Sftp.credentials.js",
|
||||
"dist/credentials/ShopifyApi.credentials.js",
|
||||
"dist/credentials/Signl4Api.credentials.js",
|
||||
"dist/credentials/Signl4Api.credentials.js",
|
||||
"dist/credentials/SlackApi.credentials.js",
|
||||
"dist/credentials/SlackOAuth2Api.credentials.js",
|
||||
"dist/credentials/Sms77Api.credentials.js",
|
||||
|
|
|
@ -818,6 +818,7 @@ export interface INodeTypeDescription extends INodeTypeBaseDescription {
|
|||
deactivate?: INodeHookDescription[];
|
||||
};
|
||||
webhooks?: IWebhookDescription[];
|
||||
translation?: { [key: string]: object };
|
||||
}
|
||||
|
||||
export interface INodeHookDescription {
|
||||
|
|