mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
✨ Render header strings
This commit is contained in:
parent
f1eef04ad2
commit
99963b04a5
|
@ -1207,6 +1207,22 @@ class App {
|
|||
),
|
||||
);
|
||||
|
||||
// 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');
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ----------------------------------------
|
||||
// Node-Types
|
||||
// ----------------------------------------
|
||||
|
|
|
@ -40,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",
|
||||
|
@ -69,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.76.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",
|
||||
|
|
|
@ -130,6 +130,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[]>;
|
||||
|
@ -147,6 +148,15 @@ export interface IRestApi {
|
|||
getTimezones(): Promise<IDataObject>;
|
||||
}
|
||||
|
||||
export interface INodeTranslationHeaders {
|
||||
data: {
|
||||
[key: string]: {
|
||||
displayName: string;
|
||||
description: string;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface IBinaryDisplayData {
|
||||
index: number;
|
||||
key: string;
|
||||
|
|
|
@ -2,7 +2,9 @@
|
|||
<div :class="$style.container">
|
||||
<el-row>
|
||||
<el-col :span="8" :class="$style.accessLabel">
|
||||
<n8n-text :compact="true" :bold="true">{{ $baseText('credentialEdit.credentialInfo.allowUseBy') }}</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $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="$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">{{ $baseText('credentialEdit.credentialInfo.created') }}</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $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">{{ $baseText('credentialEdit.credentialInfo.lastModified') }}</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $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">{{ $baseText('credentialEdit.credentialInfo.id') }}</n8n-text>
|
||||
<n8n-text :compact="true" :bold="true">
|
||||
{{ $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>
|
||||
|
@ -51,6 +62,7 @@ import { renderText } from '../mixins/renderText';
|
|||
|
||||
import TimeAgo from '../TimeAgo.vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
|
||||
export default mixins(renderText).extend({
|
||||
name: 'CredentialInfo',
|
||||
|
@ -65,6 +77,9 @@ export default mixins(renderText).extend({
|
|||
value,
|
||||
});
|
||||
},
|
||||
shortNodeType(nodeType: INodeTypeDescription) {
|
||||
return nodeType.name.replace('n8n-nodes-base.', '');
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
<template >
|
||||
<template>
|
||||
<span class="static-text-wrapper">
|
||||
<span v-show="!editActive" :title="$baseText('displayWithChange.clickToChange')">
|
||||
<span class="static-text" @mousedown="startEdit">{{currentValue}}</span>
|
||||
|
@ -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.node.type.replace('n8n-nodes-base.', '');
|
||||
|
||||
return this.$headerText({
|
||||
key: `headers.${shortNodeType}.displayName`,
|
||||
fallback: getDescendantProp(this.node, this.keyName),
|
||||
});
|
||||
}
|
||||
|
||||
return getDescendantProp(this.node, this.keyName);
|
||||
},
|
||||
},
|
||||
|
|
|
@ -114,7 +114,12 @@
|
|||
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column property="mode" :label="$baseText('executionsList.mode')" width="100" align="center"></el-table-column>
|
||||
<el-table-column property="mode" :label="$baseText('executionsList.mode')" width="100" align="center">
|
||||
<!-- TODO i18n <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 :label="$baseText('executionsList.runningTime')" width="150" align="center">
|
||||
<template slot-scope="scope">
|
||||
<span v-if="scope.row.stoppedAt === undefined">
|
||||
|
|
|
@ -1,19 +1,31 @@
|
|||
<template functional>
|
||||
<template>
|
||||
<div :class="$style.category">
|
||||
<span :class="$style.name">{{ props.item.category }}</span>
|
||||
<span :class="$style.name">
|
||||
{{ $baseText(`nodeCreator.categoryNames.${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';
|
||||
import { renderText } from '@/components/mixins/renderText';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(renderText).extend({
|
||||
props: ['item'],
|
||||
};
|
||||
computed: {
|
||||
categoryName() {
|
||||
return camelcase(this.item.category);
|
||||
},
|
||||
},
|
||||
});
|
||||
</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="$baseText('nodeCreator.mainPanel.all')" :name="ALL_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$baseText('nodeCreator.mainPanel.regular')" :name="REGULAR_NODE_FILTER"></el-tab-pane>
|
||||
<el-tab-pane :label="$baseText('nodeCreator.mainPanel.trigger')" :name="TRIGGER_NODE_FILTER"></el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
<div v-if="searchFilter.length === 0" class="scrollable">
|
||||
|
@ -55,9 +55,10 @@ import { INodeCreateElement, INodeItemProps, ISubcategoryItemProps } from '@/Int
|
|||
import { ALL_NODE_FILTER, CORE_NODES_CATEGORY, REGULAR_NODE_FILTER, TRIGGER_NODE_FILTER } from '@/constants';
|
||||
import SlideTransition from '../transitions/SlideTransition.vue';
|
||||
import { matchesNodeType, matchesSelectType } from './helpers';
|
||||
import { renderText } from '../mixins/renderText';
|
||||
|
||||
|
||||
export default mixins(externalHooks).extend({
|
||||
export default mixins(externalHooks, renderText).extend({
|
||||
name: 'NodeCreateList',
|
||||
components: {
|
||||
ItemIterator,
|
||||
|
|
|
@ -4,27 +4,31 @@
|
|||
<NoResultsIcon />
|
||||
</div>
|
||||
<div class="title">
|
||||
<div>We didn't make that... yet</div>
|
||||
<div>
|
||||
{{ $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
|
||||
{{ $baseText('nodeCreator.noResults.dontWorryYouCanProbablyDoItWithThe') }}
|
||||
<a @click="selectHttpRequest">{{ $baseText('nodeCreator.noResults.httpRequest') }}</a> or
|
||||
<a @click="selectWebhook">{{ $baseText('nodeCreator.noResults.webhook') }}</a> {{ $baseText('nodeCreator.noResults.node') }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="request">
|
||||
<div>Want us to make it faster?</div>
|
||||
<div>
|
||||
{{ $baseText('nodeCreator.noResults.wantUsToMakeItFaster') }}
|
||||
</div>
|
||||
<div>
|
||||
<a
|
||||
:href="REQUEST_NODE_FORM_URL"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Request the node</span>
|
||||
<span>{{ $baseText('nodeCreator.noResults.requestTheNode') }}</span>
|
||||
<span>
|
||||
<font-awesome-icon
|
||||
class="external"
|
||||
icon="external-link-alt"
|
||||
title="Request the node"
|
||||
:title="$baseText('nodeCreator.noResults.requestTheNode')"
|
||||
/>
|
||||
</span>
|
||||
</a>
|
||||
|
@ -37,10 +41,11 @@
|
|||
<script lang="ts">
|
||||
import { HTTP_REQUEST_NODE_TYPE, REQUEST_NODE_FORM_URL, WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
import Vue from 'vue';
|
||||
|
||||
import { renderText } from '../mixins/renderText';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import NoResultsIcon from './NoResultsIcon.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
export default mixins(renderText).extend({
|
||||
name: 'NoResults',
|
||||
components: {
|
||||
NoResultsIcon,
|
||||
|
|
|
@ -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">
|
||||
{{ $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}}
|
||||
{{ $headerText({
|
||||
key: `headers.${shortNodeType}.description`,
|
||||
fallback: nodeType.description,
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,20 +33,30 @@ import { INodeTypeDescription } from 'n8n-workflow';
|
|||
import NodeIcon from '../NodeIcon.vue';
|
||||
import TriggerIcon from '../TriggerIcon.vue';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { renderText } from '@/components/mixins/renderText';
|
||||
|
||||
Vue.component('NodeIcon', NodeIcon);
|
||||
Vue.component('TriggerIcon', TriggerIcon);
|
||||
|
||||
export default {
|
||||
export default mixins(renderText).extend({
|
||||
name: 'NodeItem',
|
||||
props: [
|
||||
'active',
|
||||
'filter',
|
||||
'nodeType',
|
||||
'bordered',
|
||||
],
|
||||
computed: {
|
||||
shortNodeType() {
|
||||
return this.nodeType.name.replace('n8n-nodes-base.', '');
|
||||
},
|
||||
},
|
||||
// @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="$baseText('nodeCreator.searchBar.searchNodes')"
|
||||
ref="input"
|
||||
:value="value"
|
||||
@input="onInput"
|
||||
|
@ -22,8 +22,9 @@
|
|||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { renderText } from '../mixins/renderText';
|
||||
|
||||
export default mixins(externalHooks).extend({
|
||||
export default mixins(externalHooks, renderText).extend({
|
||||
name: "SearchBar",
|
||||
props: ["value", "eventBus"],
|
||||
mounted() {
|
||||
|
|
|
@ -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">
|
||||
{{ $baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
||||
</div>
|
||||
<div v-if="item.properties.description" :class="$style.description">
|
||||
{{ $baseText(`nodeCreator.subcategoryDescriptions.${subcategoryDescription}`) }}
|
||||
</div>
|
||||
</div>
|
||||
<div :class="$style.action">
|
||||
|
@ -13,9 +15,22 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { renderText } from '@/components/mixins/renderText';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(renderText).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>
|
||||
{{ $baseText(`nodeCreator.subcategoryNames.${subcategoryName}`) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="scrollable">
|
||||
|
@ -18,17 +20,26 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import camelcase from 'lodash.camelcase';
|
||||
import { INodeCreateElement } from '@/Interface';
|
||||
import Vue from 'vue';
|
||||
|
||||
import ItemIterator from './ItemIterator.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
import { renderText } from '@/components/mixins/renderText';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
export default mixins(renderText).extend({
|
||||
name: 'SubcategoryPanel',
|
||||
components: {
|
||||
ItemIterator,
|
||||
},
|
||||
props: ['title', 'elements', 'activeIndex'],
|
||||
computed: {
|
||||
subcategoryName() {
|
||||
return camelcase(this.title);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
selected(element: INodeCreateElement) {
|
||||
this.$emit('selected', element);
|
||||
|
|
|
@ -95,9 +95,26 @@ export default mixins(
|
|||
|
||||
return null;
|
||||
},
|
||||
nodeTypeName(): string {
|
||||
if (this.nodeType) {
|
||||
const shortNodeType = this.nodeType.name.replace('n8n-nodes-base.', '');
|
||||
|
||||
return this.$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.nodeType.name.replace('n8n-nodes-base.', '');
|
||||
|
||||
return this.$headerText({
|
||||
key: `headers.${shortNodeType}.description`,
|
||||
fallback: this.nodeType.description,
|
||||
});
|
||||
} else {
|
||||
return this.$baseText('nodeSettings.noDescriptionFound');
|
||||
}
|
||||
|
|
|
@ -19,10 +19,19 @@ export const renderText = Vue.extend({
|
|||
},
|
||||
|
||||
/**
|
||||
* Render a string of dynamic text, i.e. a string with a constructed path to the localized value in the node text object, either in the credentials modal (`$credText`) or in the node view (`$nodeView`). **Private method**, to be called only from the two namespaces within this mixin.
|
||||
* 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.__render(arg);
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 (`$credText`), in the node view (`$nodeText`), or in the headers (`$headerText`) in the nodes panel and node view. _Private method_, to be called only from within this mixin.
|
||||
*
|
||||
* Unlike in `$baseText`, the fallback has to be set manually for dynamic text.
|
||||
*/
|
||||
__render(
|
||||
{ key, fallback }: { key: string, fallback: string },
|
||||
{ key, fallback }: { key: string; fallback: string; },
|
||||
) {
|
||||
return this.$te(key) ? this.$t(key).toString() : fallback;
|
||||
},
|
||||
|
|
|
@ -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});
|
||||
|
|
|
@ -71,7 +71,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',
|
||||
|
|
|
@ -2,7 +2,7 @@ import Vue from 'vue';
|
|||
import VueI18n from 'vue-i18n';
|
||||
import englishBaseText from './locales/en';
|
||||
import axios from 'axios';
|
||||
import path from 'path';
|
||||
import { INodeTranslationHeaders } from '@/Interface';
|
||||
|
||||
Vue.use(VueI18n);
|
||||
|
||||
|
@ -27,6 +27,7 @@ function setLanguage(language: string) {
|
|||
}
|
||||
|
||||
export async function loadLanguage(language?: string) {
|
||||
// TODO i18n: Remove next line
|
||||
console.log(`loadLanguage called with ${language}`); // eslint-disable-line no-console
|
||||
|
||||
if (!language) return Promise.resolve();
|
||||
|
@ -39,11 +40,11 @@ export async function loadLanguage(language?: string) {
|
|||
return Promise.resolve(setLanguage(language));
|
||||
}
|
||||
|
||||
const baseText = require(`./locales/${language}`).default; // TODO i18n: `path.join()`
|
||||
const baseText = require(`./locales/${language}`).default;
|
||||
i18n.setLocaleMessage(language, baseText);
|
||||
loadedLanguages.push(language);
|
||||
|
||||
return setLanguage(language);
|
||||
setLanguage(language);
|
||||
}
|
||||
|
||||
export function addNodeTranslation(
|
||||
|
@ -64,4 +65,17 @@ export function addNodeTranslation(
|
|||
language,
|
||||
Object.assign(i18n.messages[language], newNodesBase),
|
||||
);
|
||||
}
|
||||
|
||||
export function addHeaders(
|
||||
headers: INodeTranslationHeaders,
|
||||
language: string,
|
||||
) {
|
||||
i18n.setLocaleMessage(
|
||||
language,
|
||||
Object.assign(i18n.messages[language], { headers }),
|
||||
);
|
||||
|
||||
// TODO i18n: Remove next line
|
||||
console.log(i18n.messages.de.headers); // eslint-disable-line no-console
|
||||
}
|
|
@ -5,6 +5,52 @@ export default {
|
|||
clientSecret: '🇩🇪 Client Secret',
|
||||
},
|
||||
},
|
||||
nodeCreator: {
|
||||
categoryNames: {
|
||||
coreNodes: '🇩🇪 Core Nodes',
|
||||
customNodes: '🇩🇪 Custom Nodes',
|
||||
suggestedNodes: '🇩🇪 Suggested Nodes ✨',
|
||||
analytics: '🇩🇪 Analytics',
|
||||
communication: '🇩🇪 Communication',
|
||||
dataStorage: '🇩🇪 Data & Storage',
|
||||
development: '🇩🇪 Development',
|
||||
financeAccounting: '🇩🇪 Finance & Accounting',
|
||||
marketingContent: '🇩🇪 Marketing & Content',
|
||||
productivity: '🇩🇪 Productivity',
|
||||
sales: '🇩🇪 Sales',
|
||||
utility: '🇩🇪 Utility',
|
||||
miscellaneous: '🇩🇪 Miscellaneous',
|
||||
},
|
||||
subcategoryNames: {
|
||||
dataTransformation: '🇩🇪 Data Transformation',
|
||||
flow: '🇩🇪 Flow',
|
||||
files: '🇩🇪 Files',
|
||||
helpers: '🇩🇪 Helpers',
|
||||
},
|
||||
subcategoryDescriptions: {
|
||||
manipulate: '🇩🇪 Manipulate data fields, run code',
|
||||
branches: '🇩🇪 Branches, core triggers, merge data',
|
||||
work: '🇩🇪 Work with CSV, XML, text, images etc.',
|
||||
http: '🇩🇪 HTTP Requests (API calls), date and time, scrape HTML',
|
||||
},
|
||||
mainPanel: {
|
||||
all: '🇩🇪 All',
|
||||
regular: '🇩🇪 Regular',
|
||||
trigger: '🇩🇪 Trigger',
|
||||
},
|
||||
searchBar: {
|
||||
searchNodes: '🇩🇪 Search nodes...',
|
||||
},
|
||||
noResults: {
|
||||
weDidntMakeThatYet: "🇩🇪 We didn't make that... yet",
|
||||
dontWorryYouCanProbablyDoItWithThe: '🇩🇪 Don’t worry, you can probably do it with the {httpRequest} or {webhook} node',
|
||||
httpRequest: '🇩🇪 HTTP Request',
|
||||
webhook: '🇩🇪 Webhook',
|
||||
node: '🇩🇪 node',
|
||||
wantUsToMakeItFaster: '🇩🇪 Want us to make it faster?',
|
||||
requestTheNode: '🇩🇪 Request the node',
|
||||
},
|
||||
},
|
||||
textEdit: {
|
||||
edit: '🇩🇪 Edit',
|
||||
},
|
||||
|
|
|
@ -1,4 +1,50 @@
|
|||
export default {
|
||||
nodeCreator: {
|
||||
categoryNames: {
|
||||
coreNodes: 'Core Nodes',
|
||||
customNodes: 'Custom Nodes',
|
||||
suggestedNodes: 'Suggested Nodes ✨',
|
||||
analytics: 'Analytics',
|
||||
communication: 'Communication',
|
||||
dataStorage: 'Data & Storage',
|
||||
development: 'Development',
|
||||
financeAccounting: 'Finance & Accounting',
|
||||
marketingContent: 'Marketing & Content',
|
||||
productivity: 'Productivity',
|
||||
sales: 'Sales',
|
||||
utility: 'Utility',
|
||||
miscellaneous: 'Miscellaneous',
|
||||
},
|
||||
subcategoryNames: {
|
||||
dataTransformation: 'Data Transformation',
|
||||
flow: 'Flow',
|
||||
files: 'Files',
|
||||
helpers: 'Helpers',
|
||||
},
|
||||
subcategoryDescriptions: {
|
||||
manipulate: 'Manipulate data fields, run code',
|
||||
branches: 'Branches, core triggers, merge data',
|
||||
work: 'Work with CSV, XML, text, images etc.',
|
||||
http: 'HTTP Requests (API calls), date and time, scrape HTML',
|
||||
},
|
||||
mainPanel: {
|
||||
all: 'All',
|
||||
regular: 'Regular',
|
||||
trigger: 'Trigger',
|
||||
},
|
||||
searchBar: {
|
||||
searchNodes: 'Search nodes...',
|
||||
},
|
||||
noResults: {
|
||||
weDidntMakeThatYet: "We didn't make that... yet",
|
||||
dontWorryYouCanProbablyDoItWithThe: 'Don’t worry, you can probably do it with the {httpRequest} or {webhook} node',
|
||||
httpRequest: 'HTTP Request',
|
||||
webhook: 'Webhook',
|
||||
node: 'node',
|
||||
wantUsToMakeItFaster: 'Want us to make it faster?',
|
||||
requestTheNode: 'Request the node',
|
||||
},
|
||||
},
|
||||
textEdit: {
|
||||
edit: 'Edit',
|
||||
},
|
||||
|
|
|
@ -170,7 +170,11 @@ import {
|
|||
IExecutionsSummary,
|
||||
} from '../Interface';
|
||||
import { mapGetters } from 'vuex';
|
||||
import { loadLanguage, addNodeTranslation } from '@/i18n';
|
||||
import {
|
||||
loadLanguage,
|
||||
addNodeTranslation,
|
||||
addHeaders,
|
||||
} from '@/i18n';
|
||||
|
||||
const NODE_SIZE = 100;
|
||||
const DEFAULT_START_POSITION_X = 250;
|
||||
|
@ -245,9 +249,13 @@ export default mixins(
|
|||
deep: true,
|
||||
},
|
||||
|
||||
defaultLocale (newLocale, oldLocale) {
|
||||
async defaultLocale (newLocale, oldLocale) {
|
||||
// TODO i18n: Remove next line
|
||||
console.log(`Switching locale from ${oldLocale} to ${newLocale}`); // eslint-disable-line no-console
|
||||
loadLanguage(newLocale);
|
||||
|
||||
const headers = await this.restApi().getNodeTranslationHeaders();
|
||||
addHeaders(headers, this.$store.getters.defaultLocale);
|
||||
},
|
||||
},
|
||||
async beforeRouteLeave(to, from, next) {
|
||||
|
|
|
@ -1,11 +1,141 @@
|
|||
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', writeHeadersAndTranslations);
|
||||
|
||||
/**
|
||||
* Write all node translation headers at `/dist/nodes/headers.js` and write
|
||||
* each node translation at `/dist/nodes/<node>/translations/<language>.js`
|
||||
*/
|
||||
function writeHeadersAndTranslations(done) {
|
||||
checkLocale();
|
||||
|
||||
const paths = getTranslationPaths();
|
||||
const { headers, translations } = getHeadersAndTranslations(paths);
|
||||
|
||||
const headersDestinationPath = path.join(__dirname, 'dist', 'nodes', 'headers.js');
|
||||
|
||||
writeDestinationFile(headersDestinationPath, headers);
|
||||
|
||||
log('Headers translation file written to:');
|
||||
log(headersDestinationPath, { bulletpoint: true });
|
||||
|
||||
translations.forEach(t => {
|
||||
writeDestinationFile(t.destinationPath, t.content);
|
||||
});
|
||||
|
||||
log('Main translation files written to:');
|
||||
translations.forEach(t => log(t.destinationPath, { 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}.ts`,
|
||||
);
|
||||
|
||||
if (existsSync(sourcePath) && !seen[sourcePath]) {
|
||||
seen[sourcePath] = true;
|
||||
|
||||
const destinationPath = path.join(
|
||||
__dirname,
|
||||
cur.split('/').slice(0, -1).join('/'),
|
||||
'translations',
|
||||
`${locale}.js`,
|
||||
);
|
||||
|
||||
acc.push({
|
||||
source: sourcePath,
|
||||
destination: destinationPath,
|
||||
});
|
||||
};
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function getHeadersAndTranslations(paths) {
|
||||
return paths.reduce((acc, cur) => {
|
||||
const translation = require(cur.source);
|
||||
const nodeType = Object.keys(translation).pop();
|
||||
const { header } = translation[nodeType];
|
||||
|
||||
if (isValidHeader(header, ALLOWED_HEADER_KEYS)) {
|
||||
acc.headers[nodeType] = header;
|
||||
}
|
||||
|
||||
acc.translations.push({
|
||||
destinationPath: cur.destination,
|
||||
content: translation,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, { headers: {}, translations: [] });
|
||||
}
|
||||
|
||||
|
||||
// ----------------------------------
|
||||
// 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 checkLocale() {
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
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('')
|
|
@ -1,5 +1,9 @@
|
|||
module.exports = {
|
||||
bitwarden: {
|
||||
header: {
|
||||
displayName: '🇩🇪 Bitwarden',
|
||||
description: '🇩🇪 Consume Bitwarden API',
|
||||
},
|
||||
credentialsModal: {
|
||||
bitwardenApi: {
|
||||
environment: {
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
module.exports = {
|
||||
github: {
|
||||
header: {
|
||||
displayName: '🇩🇪 GitHub',
|
||||
description: '🇩🇪 Consume GitHub API',
|
||||
},
|
||||
credentialsModal: {
|
||||
githubOAuth2Api: {
|
||||
server: {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in a new issue