feat(core): Allow credential reuse on HTTP Request node (#3228)
* ✨ Create controller * ⚡ Mount controller * ✏️ Add error messages * ✨ Create scopes fetcher * ⚡ Account for non-existent credential type * 📘 Type scopes request * ⚡ Adjust error message * 🧪 Add tests * ✨ Introduce simple node versioning * ⚡ Add example how to read version in node-code for custom logic * 🐛 Fix setting of parameters * 🐛 Fix another instance where it sets the wrong parameter * ⚡ Remove unnecessary TOODs * ✨ Re-version HTTP Request node * 👕 Satisfy linter * ⚡ Retrieve node version * ⏪ Undo Jan's changes to Set node * 🧪 Fix CI/CD for `/oauth2-credential` tests (#3230) * 🐛 Fix notice warning missing background color (#3231) * 🐛 Check for generic auth in node cred types * ⚡ Refactor credentials dropdown for HTTP Request node (#3222) * ⚡ Discoverability flow (#3229) * ✨ Added node credentials type proxy. Changed node credentials input order. * ⚡ Add computed property from versioning branch * 🐛 Fix cred ref lost and unsaved * ⚡ Make options consistent with cred type names * ⚡ Use prop to set component order * ⚡ Use constant and version * ⚡ Fix rendering for generic auth creds * ⚡ Mark as required on first selection * ⚡ Implement discoverability flow * ⚡ Mark as required on subsequent selections * ⚡ Fix marking as required after cred deletion * ⚡ Refactor to clean up * ⚡ Detect position automatically * ⚡ Add i18n to option label * ⚡ Hide subtitle for custom action * ⚡ Detect active credential type * ⚡ Prop drilling to re-render select * 🔥 Remove unneeded property * ✏️ Rename arg * 🔥 Remove unused import * 🔥 Remove unneeded getters * 🔥 Remove unused import * ⚡ Generalize cred component positioning * ⚡ Set up request * 🐛 Fix edge case in endpoint * ⚡ Display scopes alert box * ⏪ Revert "Generalize cred comp positioning" This reverts commit 75eea89273b854110fa6d1f96c7c1d78dd3b0731. * ⚡ Consolidate HTTPRN check * ⚡ Fix hue percentage to degree * 🔥 Remove unused import * 🔥 Remove unused import * 🔥 Remove unused class * 🔥 Remove unused import * 📘 Create type for HTTPRN v2 auth params * ✏️ Rename check * 🔥 Remove unused import * ✏️ Add i18n to `reportUnsetCredential()` * ⚡ Refactor Alex's spacing changes * ⚡ Post-merge fixes * ⚡ Add docs link * 🔥 Exclude Notion OAuth cred * ✏️ Update copy * ✏️ Rename param * 🎨 Reposition notice and simplify styling * ✏️ Update copy * ✏️ Update copy * ⚡ Hide params during custom action * ⚡ Show notice if any cred type supported * 🐛 Prevent scopes text overflow * 🔥 Remove superfluous check * ✏️ Break up docstring * 🎨 Tweak notice styling * ⚡ Reorder cred param in Webhook node * ✏️ Shorten cred name in scopes notice * 🧪 Update Notice snapshots * 🐛 Fix check when `globalRole` is `undefined` * ⏪ Revert 3f2c4a6 * ⚡ Apply feedback from Product * 🧪 Update snapshot * ⚡ Adjust regex expansion pattern for singular * 🔥 Remove unused import * 🔥 Remove logging * ⚡ Make `somethingElse` key more unique * ⚡ Move something else to constants * ⚡ Consolidate notice component * ⚡ Apply latest feedback * 🧪 Update tests * 🧪 Update snapshot * ✏️ Fix singular version * 🧪 Finalize tests * ✏️ Rename constant * 🧪 Expand tests * 🔥 Remove `truncate` prop * 🚚 Move scopes fetching to store * 🚚 Move method to component * ⚡ Use constant * ⚡ Refactor `Notice` component * 🧪 Update tests * 🔥 Remove unused keys * ⚡ Inject custom API call option * 🔥 Remove unused props * 🎨 Use `compact` prop * 🧪 Update snapshots * 🚚 Move scopes to store * 🚚 Move `nodeCredentialTypes` to parent * ✏️ Rename cred types per branding * 🐛 Clear scopes when none * ⚡ Add default * 🚚 Move `newHttpRequestNodeCredentialType` to parent * 🔥 Remove test data * ⚡ Separate lines for readability * ⚡ Change reference from node to node name * ✏️ Rename i18n keys * ⚡ Refactor OAuth check * 🔥 Remove unused key * 🚚 Move `OAuth1/2 API` to i18n * ⚡ Refactor `skipCheck` * ⚡ Add `stopPropagation` and `preventDefault` * 🚚 Move active credential scopes logic to store * 🎨 Fix spacing for `NodeWebhooks` component * ⚡ Implement feedback * ⚡ Update HTTPRN default and issue copy * Refactor to use `CredentialsSelect` param (#3304) * ⚡ Refactor into cred type param * ⚡ Componentize scopes notice * 🔥 Remove unused data * 🔥 Remove unused `loadOptions` * ⚡ Componentize `NodeCredentialType` * 🐛 Fix param validation * 🔥 Remove dup methods * ⚡ Refactor all references to `isHttpRequestNodeV2` * 🎨 Fix styling * 🔥 Remove unused import * 🔥 Remove unused properties * 🎨 Fix spacing for Pipedrive Trigger node * 🎨 Undo Webhook node styling change * 🔥 Remove unused style * ⚡ Cover `httpHeaderAuth` edge case * 🐛 Fix `this.node` reference * 🚚 Rename to `credentialsSelect` * 🐛 Fix mistaken renaming * ⚡ Set one attribute per line * ⚡ Move condition to instantiation site * 🚚 Rename prop * ⚡ Refactor away `prepareScopesNotice` * ✏️ Rename i18n keys * ✏️ Update i18n calls * ✏️ Add more i18n keys * 🔥 Remove unused props * ✏️ Add explanatory comment * ⚡ Adjust check in `hasProxyAuth` * ⚡ Refactor `credentialSelected` from prop to event * ⚡ Eventify `valueChanged`, `setFocus`, `onBlur` * ⚡ Eventify `optionSelected` * ⚡ Add `noDataExpression` * 🔥 Remove logging * 🔥 Remove URL from scopes * ⚡ Disregard expressions for display * 🎨 Use CSS modules * 📘 Tigthen interface * 🐛 Fix generic auth display * 🐛 Fix generic auth validation * 📘 Loosen type * 🚚 Move event params to end * ⚡ Generalize reference * ⚡ Refactor generic auth as `credentialsSelect` param * ⏪ Restore check for `httpHeaderAuth ` * 🚚 Rename `existing` to `predefined` * Extend metrics for HTTP Request node (#3282) * ⚡ Extend metrics * 🧪 Add tests * ⚡ Update param names Co-authored-by: Alex Grozav <alex@grozav.com> * ⚡ Update check per new branch * ⚡ Include generic auth check * ⚡ Adjust telemetry (#3359) * ⚡ Filter credential types by label Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
@@ -159,6 +159,9 @@ export interface IExternalHooks {
|
||||
run(eventName: string, metadata?: IDataObject): Promise<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Do not add methods to this interface.
|
||||
*/
|
||||
export interface IRestApi {
|
||||
getActiveWorkflows(): Promise<string[]>;
|
||||
getActivationError(id: string): Promise<IActivationError | undefined >;
|
||||
|
||||
153
packages/editor-ui/src/components/CredentialsSelect.vue
Normal file
153
packages/editor-ui/src/components/CredentialsSelect.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<div>
|
||||
<div :class="$style['parameter-value-container']">
|
||||
<n8n-select
|
||||
:size="inputSize"
|
||||
filterable
|
||||
:value="displayValue"
|
||||
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')"
|
||||
:title="displayTitle"
|
||||
@change="(value) => $emit('valueChanged', value)"
|
||||
@keydown.stop
|
||||
@focus="$emit('setFocus')"
|
||||
@blur="$emit('onBlur')"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="credType in supportedCredentialTypes"
|
||||
:value="credType.name"
|
||||
:key="credType.name"
|
||||
:label="credType.displayName"
|
||||
>
|
||||
<div class="list-option">
|
||||
<div class="option-headline">
|
||||
{{ credType.displayName }}
|
||||
</div>
|
||||
<div
|
||||
v-if="credType.description"
|
||||
class="option-description"
|
||||
v-html="credType.description"
|
||||
/>
|
||||
</div>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
<slot name="issues-and-options" />
|
||||
</div>
|
||||
|
||||
<scopes-notice
|
||||
v-if="scopes.length > 0"
|
||||
:activeCredentialType="activeCredentialType"
|
||||
:scopes="scopes"
|
||||
/>
|
||||
<div>
|
||||
<node-credentials
|
||||
:node="node"
|
||||
:overrideCredType="node.parameters[parameter.name]"
|
||||
@credentialSelected="(updateInformation) => $emit('credentialSelected', updateInformation)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { ICredentialType } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
import ScopesNotice from '@/components/ScopesNotice.vue';
|
||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'CredentialsSelect',
|
||||
components: {
|
||||
ScopesNotice,
|
||||
NodeCredentials,
|
||||
},
|
||||
props: [
|
||||
'activeCredentialType',
|
||||
'node',
|
||||
'parameter',
|
||||
'inputSize',
|
||||
'displayValue',
|
||||
'isReadOnly',
|
||||
'displayTitle',
|
||||
],
|
||||
computed: {
|
||||
...mapGetters('credentials', ['allCredentialTypes', 'getScopesByCredentialType']),
|
||||
scopes(): string[] {
|
||||
if (!this.activeCredentialType) return [];
|
||||
|
||||
return this.getScopesByCredentialType(this.activeCredentialType);
|
||||
},
|
||||
supportedCredentialTypes(): ICredentialType[] {
|
||||
return this.allCredentialTypes.filter((c: ICredentialType) => this.isSupported(c.name));
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Check if a credential type belongs to one of the supported sets defined
|
||||
* in the `credentialTypes` key in a `credentialsSelect` parameter
|
||||
*/
|
||||
isSupported(name: string): boolean {
|
||||
const supported = this.getSupportedSets(this.parameter.credentialTypes);
|
||||
|
||||
const checkedCredType = this.$store.getters['credentials/getCredentialTypeByName'](name);
|
||||
|
||||
for (const property of supported.has) {
|
||||
if (checkedCredType[property] !== undefined) {
|
||||
|
||||
// edge case: `httpHeaderAuth` has `authenticate` auth but belongs to generic auth
|
||||
if (name === 'httpHeaderAuth' && property === 'authenticate') continue;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
checkedCredType.extends &&
|
||||
checkedCredType.extends.some(
|
||||
(parentType: string) => supported.extends.includes(parentType),
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (checkedCredType.extends && supported.extends.length) {
|
||||
// recurse upward until base credential type
|
||||
// e.g. microsoftDynamicsOAuth2Api -> microsoftOAuth2Api -> oAuth2Api
|
||||
return checkedCredType.extends.reduce(
|
||||
(acc: boolean, parentType: string) => acc || this.isSupported(parentType),
|
||||
false,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
getSupportedSets(credentialTypes: string[]) {
|
||||
return credentialTypes.reduce<{ extends: string[]; has: string[] }>((acc, cur) => {
|
||||
const _extends = cur.split('extends:');
|
||||
|
||||
if (_extends.length === 2) {
|
||||
acc.extends.push(_extends[1]);
|
||||
return acc;
|
||||
}
|
||||
|
||||
const _has = cur.split('has:');
|
||||
|
||||
if (_has.length === 2) {
|
||||
acc.has.push(_has[1]);
|
||||
return acc;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, { extends: [], has: [] });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.parameter-value-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -75,7 +75,7 @@
|
||||
<script lang="ts">
|
||||
|
||||
import Vue from 'vue';
|
||||
import { WAIT_TIME_UNLIMITED } from '@/constants';
|
||||
import { CUSTOM_API_CALL_KEY, WAIT_TIME_UNLIMITED } from '@/constants';
|
||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||
import { nodeBase } from '@/components/mixins/nodeBase';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
@@ -336,7 +336,11 @@ export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).ext
|
||||
},
|
||||
methods: {
|
||||
setSubtitle() {
|
||||
this.nodeSubtitle = this.getNodeSubtitle(this.data, this.nodeType, this.getWorkflow()) || '';
|
||||
const nodeSubtitle = this.getNodeSubtitle(this.data, this.nodeType, this.getWorkflow()) || '';
|
||||
|
||||
this.nodeSubtitle = nodeSubtitle.includes(CUSTOM_API_CALL_KEY)
|
||||
? ''
|
||||
: nodeSubtitle;
|
||||
},
|
||||
disableNode () {
|
||||
this.disableNodes([this.data]);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div v-if="credentialTypesNodeDescriptionDisplayed.length" :class="$style.container">
|
||||
<div v-if="credentialTypesNodeDescriptionDisplayed.length" :class="['node-credentials', $style.container]">
|
||||
<div v-for="credentialTypeDescription in credentialTypesNodeDescriptionDisplayed" :key="credentialTypeDescription.name">
|
||||
<n8n-input-label
|
||||
:label="$locale.baseText(
|
||||
@@ -11,15 +11,20 @@
|
||||
}
|
||||
)"
|
||||
:bold="false"
|
||||
size="small"
|
||||
|
||||
:set="issues = getIssues(credentialTypeDescription.name)"
|
||||
size="small"
|
||||
>
|
||||
<div v-if="isReadOnly">
|
||||
<n8n-input disabled :value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name" size="small" />
|
||||
<n8n-input
|
||||
:value="selected && selected[credentialTypeDescription.name] && selected[credentialTypeDescription.name].name"
|
||||
disabled
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div :class="issues.length ? $style.hasIssues : $style.input" v-else >
|
||||
<div
|
||||
v-else
|
||||
:class="issues.length ? $style.hasIssues : $style.input"
|
||||
>
|
||||
<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]"
|
||||
@@ -82,6 +87,7 @@ export default mixins(
|
||||
name: 'NodeCredentials',
|
||||
props: [
|
||||
'node', // INodeUi
|
||||
'overrideCredType', // cred type
|
||||
],
|
||||
data () {
|
||||
return {
|
||||
@@ -92,6 +98,7 @@ export default mixins(
|
||||
computed: {
|
||||
...mapGetters('credentials', {
|
||||
credentialOptions: 'allCredentialsByType',
|
||||
getCredentialTypeByName: 'getCredentialTypeByName',
|
||||
}),
|
||||
credentialTypesNode (): string[] {
|
||||
return this.credentialTypesNodeDescription
|
||||
@@ -106,6 +113,10 @@ export default mixins(
|
||||
credentialTypesNodeDescription (): INodeCredentialDescription[] {
|
||||
const node = this.node as INodeUi;
|
||||
|
||||
const credType = this.getCredentialTypeByName(this.overrideCredType);
|
||||
|
||||
if (credType) return [credType];
|
||||
|
||||
const activeNodeType = this.$store.getters.nodeType(node.type, node.typeVersion) as INodeTypeDescription | null;
|
||||
if (activeNodeType && activeNodeType.credentials) {
|
||||
return activeNodeType.credentials;
|
||||
@@ -198,7 +209,15 @@ export default mixins(
|
||||
return;
|
||||
}
|
||||
|
||||
this.$telemetry.track('User selected credential from node modal', { credential_type: credentialType, workflow_id: this.$store.getters.workflowId });
|
||||
this.$telemetry.track(
|
||||
'User selected credential from node modal',
|
||||
{
|
||||
credential_type: credentialType,
|
||||
node_type: this.node.type,
|
||||
...(this.hasProxyAuth(this.node) ? { is_service_specific: true } : {}),
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
},
|
||||
);
|
||||
|
||||
const selectedCredentials = this.$store.getters['credentials/getCredentialById'](credentialId);
|
||||
const oldCredentials = this.node.credentials && this.node.credentials[credentialType] ? this.node.credentials[credentialType] : {};
|
||||
@@ -295,11 +314,7 @@ export default mixins(
|
||||
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
margin: var(--spacing-xs) 0;
|
||||
|
||||
> * {
|
||||
margin-bottom: var(--spacing-xs);
|
||||
}
|
||||
margin-top: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.warning {
|
||||
|
||||
@@ -23,14 +23,35 @@
|
||||
</div>
|
||||
<div class="node-parameters-wrapper" v-if="node && nodeValid">
|
||||
<div v-show="openPanel === 'params'">
|
||||
<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" />
|
||||
<node-webhooks
|
||||
:node="node"
|
||||
:nodeType="nodeType"
|
||||
/>
|
||||
<parameter-input-list
|
||||
:parameters="parametersNoneSetting"
|
||||
:hideDelete="true"
|
||||
:nodeValues="nodeValues" path="parameters" @valueChanged="valueChanged"
|
||||
>
|
||||
<node-credentials
|
||||
:node="node"
|
||||
@credentialSelected="credentialSelected"
|
||||
/>
|
||||
</parameter-input-list>
|
||||
<div v-if="parametersNoneSetting.length === 0" class="no-parameters">
|
||||
<n8n-text>
|
||||
{{ $locale.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
|
||||
{{ $locale.baseText('nodeSettings.thisNodeDoesNotHaveAnyParameters') }}
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-if="isCustomApiCallSelected(nodeValues)" class="parameter-item parameter-notice">
|
||||
<n8n-notice
|
||||
:content="$locale.baseText(
|
||||
'nodeSettings.useTheHttpRequestNode',
|
||||
{ interpolate: { nodeTypeDisplayName: nodeType.displayName } }
|
||||
)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div v-show="openPanel === 'settings'">
|
||||
<parameter-input-list :parameters="nodeSettings" :hideDelete="true" :nodeValues="nodeValues" path="" @valueChanged="valueChanged" />
|
||||
|
||||
@@ -104,12 +104,44 @@
|
||||
:placeholder="parameter.placeholder"
|
||||
/>
|
||||
|
||||
<credentials-select
|
||||
v-else-if="parameter.type === 'credentialsSelect' || (parameter.name === 'genericAuthType')"
|
||||
ref="inputField"
|
||||
:parameter="parameter"
|
||||
:node="node"
|
||||
:activeCredentialType="activeCredentialType"
|
||||
:inputSize="inputSize"
|
||||
:displayValue="displayValue"
|
||||
:isReadOnly="isReadOnly"
|
||||
:displayTitle="displayTitle"
|
||||
@credentialSelected="credentialSelected"
|
||||
@valueChanged="valueChanged"
|
||||
@setFocus="setFocus"
|
||||
@onBlur="onBlur"
|
||||
>
|
||||
<template v-slot:issues-and-options>
|
||||
<parameter-issues
|
||||
:issues="getIssues"
|
||||
/>
|
||||
<parameter-options
|
||||
v-if="displayOptionsComputed"
|
||||
:displayOptionsComputed="displayOptionsComputed"
|
||||
:parameter="parameter"
|
||||
:isValueExpression="isValueExpression"
|
||||
:isDefault="isDefault"
|
||||
:hasRemoteMethod="hasRemoteMethod"
|
||||
@optionSelected="optionSelected"
|
||||
/>
|
||||
</template>
|
||||
</credentials-select>
|
||||
|
||||
<n8n-select
|
||||
v-else-if="parameter.type === 'options'"
|
||||
ref="inputField"
|
||||
:size="inputSize"
|
||||
filterable
|
||||
:value="displayValue"
|
||||
:placeholder="parameter.placeholder ? getPlaceholder() : $locale.baseText('parameterInput.select')"
|
||||
:loading="remoteParameterOptionsLoading"
|
||||
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
||||
:title="displayTitle"
|
||||
@@ -168,26 +200,21 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="parameter-issues" v-if="getIssues.length">
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="`${$locale.baseText('parameterInput.issues')}:<br /> - ` + getIssues.join('<br /> - ')"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
<parameter-issues
|
||||
v-if="parameter.type !== 'credentialsSelect'"
|
||||
:issues="getIssues"
|
||||
/>
|
||||
|
||||
<parameter-options
|
||||
v-if="displayOptionsComputed && parameter.type !== 'credentialsSelect'"
|
||||
:displayOptionsComputed="displayOptionsComputed"
|
||||
:parameter="parameter"
|
||||
:isValueExpression="isValueExpression"
|
||||
:isDefault="isDefault"
|
||||
:hasRemoteMethod="hasRemoteMethod"
|
||||
@optionSelected="optionSelected"
|
||||
/>
|
||||
|
||||
<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="$locale.baseText('parameterInput.parameterOptions')"/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<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="hasRemoteMethod">{{ $locale.baseText('parameterInput.refreshList') }}</el-dropdown-item>
|
||||
<el-dropdown-item command="resetValue" :disabled="isDefault" :divided="!parameter.noDataExpression || hasRemoteMethod">{{ $locale.baseText('parameterInput.resetValue') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -196,6 +223,7 @@ import { get } from 'lodash';
|
||||
|
||||
import {
|
||||
INodeUi,
|
||||
INodeUpdatePropertiesInformation,
|
||||
} from '@/Interface';
|
||||
import {
|
||||
NodeHelpers,
|
||||
@@ -208,7 +236,12 @@ import {
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import CodeEdit from '@/components/CodeEdit.vue';
|
||||
import CredentialsSelect from '@/components/CredentialsSelect.vue';
|
||||
import ExpressionEdit from '@/components/ExpressionEdit.vue';
|
||||
import NodeCredentials from '@/components/NodeCredentials.vue';
|
||||
import ScopesNotice from '@/components/ScopesNotice.vue';
|
||||
import ParameterOptions from '@/components/ParameterOptions.vue';
|
||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||
// @ts-ignore
|
||||
import PrismEditor from 'vue-prism-editor';
|
||||
import TextEdit from '@/components/TextEdit.vue';
|
||||
@@ -218,6 +251,8 @@ import { showMessage } from '@/components/mixins/showMessage';
|
||||
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default mixins(
|
||||
externalHooks,
|
||||
@@ -230,7 +265,12 @@ export default mixins(
|
||||
components: {
|
||||
CodeEdit,
|
||||
ExpressionEdit,
|
||||
NodeCredentials,
|
||||
CredentialsSelect,
|
||||
PrismEditor,
|
||||
ScopesNotice,
|
||||
ParameterOptions,
|
||||
ParameterIssues,
|
||||
TextEdit,
|
||||
},
|
||||
props: [
|
||||
@@ -257,6 +297,8 @@ export default mixins(
|
||||
remoteParameterOptionsLoadingIssues: null as string | null,
|
||||
textEditDialogVisible: false,
|
||||
tempValue: '', // el-date-picker and el-input does not seem to work without v-model so add one
|
||||
CUSTOM_API_CALL_KEY,
|
||||
activeCredentialType: '',
|
||||
dateTimePickerOptions: {
|
||||
shortcuts: [
|
||||
{
|
||||
@@ -303,6 +345,7 @@ export default mixins(
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters('credentials', ['allCredentialTypes']),
|
||||
areExpressionsDisabled(): boolean {
|
||||
return this.$store.getters['ui/areExpressionsDisabled'];
|
||||
},
|
||||
@@ -373,6 +416,13 @@ export default mixins(
|
||||
returnValue = this.expressionValueComputed;
|
||||
}
|
||||
|
||||
if (this.parameter.type === 'credentialsSelect') {
|
||||
const credType = this.$store.getters['credentials/getCredentialTypeByName'](this.value);
|
||||
if (credType) {
|
||||
returnValue = credType.displayName;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && returnValue.charAt(0) === '#') {
|
||||
// Convert the value to rgba that el-color-picker can display it correctly
|
||||
const bigint = parseInt(returnValue.slice(1), 16);
|
||||
@@ -471,7 +521,17 @@ export default mixins(
|
||||
|
||||
const issues = NodeHelpers.getParameterIssues(this.parameter, this.node.parameters, newPath.join('.'), this.node);
|
||||
|
||||
if (['options', 'multiOptions'].includes(this.parameter.type) && this.remoteParameterOptionsLoading === false && this.remoteParameterOptionsLoadingIssues === null) {
|
||||
if (this.parameter.type === 'credentialsSelect' && this.displayValue === '') {
|
||||
issues.parameters = issues.parameters || {};
|
||||
|
||||
const issue = this.$locale.baseText('parameterInput.selectACredentialTypeFromTheDropdown');
|
||||
|
||||
issues.parameters[this.parameter.name] = [issue];
|
||||
} else if (
|
||||
['options', 'multiOptions'].includes(this.parameter.type) &&
|
||||
this.remoteParameterOptionsLoading === false &&
|
||||
this.remoteParameterOptionsLoadingIssues === null
|
||||
) {
|
||||
// Check if the value resolves to a valid option
|
||||
// Currently it only displays an error in the node itself in
|
||||
// case the value is not valid. The workflow can still be executed
|
||||
@@ -479,18 +539,28 @@ export default mixins(
|
||||
const validOptions = this.parameterOptions!.map((options: INodePropertyOptions) => options.value);
|
||||
|
||||
const checkValues: string[] = [];
|
||||
if (Array.isArray(this.displayValue)) {
|
||||
checkValues.push.apply(checkValues, this.displayValue);
|
||||
} else {
|
||||
checkValues.push(this.displayValue as string);
|
||||
|
||||
if (!this.skipCheck(this.displayValue)) {
|
||||
if (Array.isArray(this.displayValue)) {
|
||||
checkValues.push.apply(checkValues, this.displayValue);
|
||||
} else {
|
||||
checkValues.push(this.displayValue as string);
|
||||
}
|
||||
}
|
||||
|
||||
for (const checkValue of checkValues) {
|
||||
if (checkValue !== undefined && checkValue.includes(CUSTOM_API_CALL_KEY)) continue;
|
||||
if (checkValue === null || !validOptions.includes(checkValue)) {
|
||||
if (issues.parameters === undefined) {
|
||||
issues.parameters = {};
|
||||
}
|
||||
issues.parameters[this.parameter.name] = [`The value "${checkValue}" is not supported!`];
|
||||
|
||||
const issue = this.$locale.baseText(
|
||||
'parameterInput.theValueIsNotSupported',
|
||||
{ interpolate: { checkValue } },
|
||||
);
|
||||
|
||||
issues.parameters[this.parameter.name] = [issue];
|
||||
}
|
||||
}
|
||||
} else if (this.remoteParameterOptionsLoadingIssues !== null) {
|
||||
@@ -557,6 +627,9 @@ export default mixins(
|
||||
const styles = {
|
||||
width: '100%',
|
||||
};
|
||||
if (this.parameter.type === 'credentialsSelect') {
|
||||
return styles;
|
||||
}
|
||||
if (this.displayOptionsComputed === true) {
|
||||
deductWidth += 25;
|
||||
}
|
||||
@@ -583,6 +656,23 @@ export default mixins(
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
credentialSelected (updateInformation: INodeUpdatePropertiesInformation) {
|
||||
// Update the values on the node
|
||||
this.$store.commit('updateNodeProperties', updateInformation);
|
||||
|
||||
const node = this.$store.getters.getNodeByName(updateInformation.name);
|
||||
|
||||
// Update the issues
|
||||
this.updateNodeCredentialIssues(node);
|
||||
|
||||
this.$externalHooks().run('nodeSettings.credentialSelected', { updateInformation });
|
||||
},
|
||||
/**
|
||||
* Check whether a param value must be skipped when collecting node param issues for validation.
|
||||
*/
|
||||
skipCheck(value: string | number | boolean | null) {
|
||||
return typeof value === 'string' && value.includes(CUSTOM_API_CALL_KEY);
|
||||
},
|
||||
getPlaceholder(): string {
|
||||
return this.isForCredential
|
||||
? this.$locale.credText().placeholder(this.parameter)
|
||||
@@ -737,6 +827,10 @@ export default mixins(
|
||||
this.$emit('textInput', parameterData);
|
||||
},
|
||||
valueChanged (value: string[] | string | number | boolean | Date | null) {
|
||||
if (this.parameter.name === 'nodeCredentialType') {
|
||||
this.activeCredentialType = value as string;
|
||||
}
|
||||
|
||||
if (value instanceof Date) {
|
||||
value = value.toISOString();
|
||||
}
|
||||
@@ -790,6 +884,10 @@ export default mixins(
|
||||
this.nodeName = this.node.name;
|
||||
}
|
||||
|
||||
if (this.node && this.node.parameters.authentication === 'predefinedCredentialType') {
|
||||
this.activeCredentialType = this.node.parameters.nodeCredentialType as string;
|
||||
}
|
||||
|
||||
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true && this.displayValue !== null && this.displayValue.toString().charAt(0) !== '#') {
|
||||
const newValue = this.rgbaToHex(this.displayValue as string);
|
||||
if (newValue !== null) {
|
||||
@@ -856,20 +954,6 @@ export default mixins(
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.parameter-options {
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.parameter-issues {
|
||||
width: 20px;
|
||||
text-align: right;
|
||||
float: right;
|
||||
color: #ff8080;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
|
||||
::v-deep .color-input {
|
||||
display: flex;
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div class="paramter-input-list-wrapper">
|
||||
<div v-for="parameter in filteredParameters" :key="parameter.name" :class="{indent}">
|
||||
<div class="parameter-input-list-wrapper">
|
||||
<div v-for="(parameter, index) in filteredParameters" :key="parameter.name">
|
||||
<slot v-if="indexToShowSlotAt === index" />
|
||||
|
||||
<div
|
||||
v-if="multipleValues(parameter) === true && parameter.type !== 'fixedCollection'"
|
||||
class="parameter-item"
|
||||
@@ -18,8 +20,6 @@
|
||||
v-else-if="parameter.type === 'notice'"
|
||||
class="parameter-item"
|
||||
:content="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||
:truncate="parameter.typeOptions && parameter.typeOptions.truncate"
|
||||
:truncate-at="parameter.typeOptions && parameter.typeOptions.truncateAt"
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -101,6 +101,7 @@ import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import { get, set } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { WEBHOOK_NODE_TYPE } from '@/constants';
|
||||
|
||||
export default mixins(
|
||||
genericHelpers,
|
||||
@@ -129,6 +130,11 @@ export default mixins(
|
||||
node (): INodeUi {
|
||||
return this.$store.getters.activeNode;
|
||||
},
|
||||
indexToShowSlotAt (): number {
|
||||
if (this.node.type === WEBHOOK_NODE_TYPE) return 1;
|
||||
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
multipleValues (parameter: INodeProperties): boolean {
|
||||
@@ -164,11 +170,27 @@ export default mixins(
|
||||
|
||||
this.$emit('valueChanged', parameterData);
|
||||
},
|
||||
|
||||
mustHideDuringCustomApiCall (parameter: INodeProperties, nodeValues: INodeParameters): boolean {
|
||||
if (parameter && parameter.displayOptions && parameter.displayOptions.hide) return true;
|
||||
|
||||
const MUST_REMAIN_VISIBLE = ['authentication', 'resource', 'operation', ...Object.keys(nodeValues)];
|
||||
|
||||
return !MUST_REMAIN_VISIBLE.includes(parameter.name);
|
||||
},
|
||||
|
||||
displayNodeParameter (parameter: INodeProperties): boolean {
|
||||
if (parameter.type === 'hidden') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.isCustomApiCallSelected(this.nodeValues) &&
|
||||
this.mustHideDuringCustomApiCall(parameter, this.nodeValues)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parameter.displayOptions === undefined) {
|
||||
// If it is not defined no need to do a proper check
|
||||
return true;
|
||||
@@ -260,7 +282,7 @@ export default mixins(
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.paramter-input-list-wrapper {
|
||||
.parameter-input-list-wrapper {
|
||||
.delete-option {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
30
packages/editor-ui/src/components/ParameterIssues.vue
Normal file
30
packages/editor-ui/src/components/ParameterIssues.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div :class="$style['parameter-issues']" v-if="issues.length">
|
||||
<n8n-tooltip placement="top" >
|
||||
<div slot="content" v-html="`${$locale.baseText('parameterInput.issues')}:<br /> - ` + issues.join('<br /> - ')"></div>
|
||||
<font-awesome-icon icon="exclamation-triangle" />
|
||||
</n8n-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ParameterIssues',
|
||||
props: [
|
||||
'issues',
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.parameter-issues {
|
||||
width: 20px;
|
||||
text-align: right;
|
||||
float: right;
|
||||
color: #ff8080;
|
||||
font-size: var(--font-size-s);
|
||||
padding-left: var(--spacing-4xs);
|
||||
}
|
||||
</style>
|
||||
68
packages/editor-ui/src/components/ParameterOptions.vue
Normal file
68
packages/editor-ui/src/components/ParameterOptions.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<template>
|
||||
<div :class="$style['parameter-options']">
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="(opt) => $emit('optionSelected', opt)"
|
||||
size="mini"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<font-awesome-icon
|
||||
icon="cogs"
|
||||
class="reset-icon clickable"
|
||||
:title="$locale.baseText('parameterInput.parameterOptions')"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item
|
||||
v-if="parameter.noDataExpression !== true && !isValueExpression"
|
||||
command="addExpression"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.addExpression') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="parameter.noDataExpression !== true && isValueExpression"
|
||||
command="removeExpression"
|
||||
>
|
||||
{{ $locale.baseText('parameterInput.removeExpression') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item
|
||||
v-if="hasRemoteMethod"
|
||||
command="refreshOptions"
|
||||
>
|
||||
{{ $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>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ParameterOptions',
|
||||
props: [
|
||||
'displayOptionsComputed',
|
||||
'optionSelected',
|
||||
'parameter',
|
||||
'isValueExpression',
|
||||
'isDefault',
|
||||
'hasRemoteMethod',
|
||||
],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style module lang="scss">
|
||||
.parameter-options {
|
||||
width: 25px;
|
||||
text-align: right;
|
||||
float: right;
|
||||
}
|
||||
</style>
|
||||
55
packages/editor-ui/src/components/ScopesNotice.vue
Normal file
55
packages/editor-ui/src/components/ScopesNotice.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<n8n-notice
|
||||
:content="scopesShortContent"
|
||||
:fullContent="scopesFullContent"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ScopesNotice',
|
||||
props: [
|
||||
'activeCredentialType',
|
||||
'scopes',
|
||||
],
|
||||
computed: {
|
||||
...mapGetters('credentials', ['getCredentialTypeByName']),
|
||||
scopesShortContent (): string {
|
||||
return this.$locale.baseText(
|
||||
'nodeSettings.scopes.notice',
|
||||
{
|
||||
adjustToNumber: this.scopes.length,
|
||||
interpolate: {
|
||||
activeCredential: this.shortCredentialDisplayName,
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
scopesFullContent (): string {
|
||||
return this.$locale.baseText(
|
||||
'nodeSettings.scopes.expandedNoticeWithScopes',
|
||||
{
|
||||
adjustToNumber: this.scopes.length,
|
||||
interpolate: {
|
||||
activeCredential: this.shortCredentialDisplayName,
|
||||
scopes: this.scopes.map(
|
||||
(s: string) => s.includes('/') ? s.split('/').pop() : s,
|
||||
).join('<br>'),
|
||||
},
|
||||
},
|
||||
);
|
||||
},
|
||||
shortCredentialDisplayName (): string {
|
||||
const oauth1Api = this.$locale.baseText('generic.oauth1Api');
|
||||
const oauth2Api = this.$locale.baseText('generic.oauth2Api');
|
||||
|
||||
return this.getCredentialTypeByName(this.activeCredentialType).displayName
|
||||
.replace(new RegExp(`${oauth1Api}|${oauth2Api}`), '')
|
||||
.trim();
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
CUSTOM_API_CALL_KEY,
|
||||
} from '@/constants';
|
||||
|
||||
import {
|
||||
@@ -32,12 +33,30 @@ import { restApi } from '@/components/mixins/restApi';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export const nodeHelpers = mixins(
|
||||
restApi,
|
||||
)
|
||||
.extend({
|
||||
computed: {
|
||||
...mapGetters('credentials', [ 'getCredentialTypeByName', 'getCredentialsByType' ]),
|
||||
},
|
||||
methods: {
|
||||
hasProxyAuth (node: INodeUi): boolean {
|
||||
return Object.keys(node.parameters).includes('nodeCredentialType');
|
||||
},
|
||||
|
||||
isCustomApiCallSelected (nodeValues: INodeParameters): boolean {
|
||||
const { parameters } = nodeValues;
|
||||
|
||||
if (!isObjectLiteral(parameters)) return false;
|
||||
|
||||
return (
|
||||
parameters.resource !== undefined && parameters.resource.includes(CUSTOM_API_CALL_KEY) ||
|
||||
parameters.operation !== undefined && parameters.operation.includes(CUSTOM_API_CALL_KEY)
|
||||
);
|
||||
},
|
||||
|
||||
// Returns the parameter value
|
||||
getParameterValue (nodeValues: INodeParameters, parameterName: string, path: string) {
|
||||
@@ -116,6 +135,23 @@ export const nodeHelpers = mixins(
|
||||
return false;
|
||||
},
|
||||
|
||||
reportUnsetCredential(credentialType: ICredentialType) {
|
||||
return {
|
||||
credentials: {
|
||||
[credentialType.name]: [
|
||||
this.$locale.baseText(
|
||||
'nodeHelpers.credentialsUnset',
|
||||
{
|
||||
interpolate: {
|
||||
credentialType: credentialType.displayName,
|
||||
},
|
||||
},
|
||||
),
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
// Updates the execution issues.
|
||||
updateNodesExecutionIssues () {
|
||||
const nodes = this.$store.getters.allNodes;
|
||||
@@ -198,6 +234,46 @@ export const nodeHelpers = mixins(
|
||||
let credentialType: ICredentialType | null;
|
||||
let credentialDisplayName: string;
|
||||
let selectedCredentials: INodeCredentialsDetails;
|
||||
|
||||
const {
|
||||
authentication,
|
||||
genericAuthType,
|
||||
nodeCredentialType,
|
||||
} = node.parameters as HttpRequestNode.V2.AuthParams;
|
||||
|
||||
if (
|
||||
authentication === 'genericCredentialType' &&
|
||||
genericAuthType !== '' &&
|
||||
selectedCredsAreUnusable(node, genericAuthType)
|
||||
) {
|
||||
const credential = this.getCredentialTypeByName(genericAuthType);
|
||||
return this.reportUnsetCredential(credential);
|
||||
}
|
||||
|
||||
if (
|
||||
this.hasProxyAuth(node) &&
|
||||
authentication === 'predefinedCredentialType' &&
|
||||
nodeCredentialType !== '' &&
|
||||
node.credentials !== undefined
|
||||
) {
|
||||
const stored = this.getCredentialsByType(nodeCredentialType);
|
||||
|
||||
if (selectedCredsDoNotExist(node, nodeCredentialType, stored)) {
|
||||
const credential = this.getCredentialTypeByName(nodeCredentialType);
|
||||
return this.reportUnsetCredential(credential);
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
this.hasProxyAuth(node) &&
|
||||
authentication === 'predefinedCredentialType' &&
|
||||
nodeCredentialType !== '' &&
|
||||
selectedCredsAreUnusable(node, nodeCredentialType)
|
||||
) {
|
||||
const credential = this.getCredentialTypeByName(nodeCredentialType);
|
||||
return this.reportUnsetCredential(credential);
|
||||
}
|
||||
|
||||
for (const credentialTypeDescription of nodeType!.credentials!) {
|
||||
// Check if credentials should be displayed else ignore
|
||||
if (this.displayParameter(node.parameters, credentialTypeDescription, '', node) !== true) {
|
||||
@@ -398,3 +474,43 @@ export const nodeHelpers = mixins(
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether the node has no selected credentials, or none of the node's
|
||||
* selected credentials are of the specified type.
|
||||
*/
|
||||
function selectedCredsAreUnusable(node: INodeUi, credentialType: string) {
|
||||
return node.credentials === undefined || Object.keys(node.credentials).includes(credentialType) === false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the node's selected credentials of the specified type
|
||||
* can no longer be found in the database.
|
||||
*/
|
||||
function selectedCredsDoNotExist(
|
||||
node: INodeUi,
|
||||
nodeCredentialType: string,
|
||||
storedCredsByType: ICredentialsResponse[] | null,
|
||||
) {
|
||||
if (!node.credentials || !storedCredsByType) return false;
|
||||
|
||||
const selectedCredsByType = node.credentials[nodeCredentialType];
|
||||
|
||||
if (!selectedCredsByType) return false;
|
||||
|
||||
return !storedCredsByType.find((c) => c.id === selectedCredsByType.id);
|
||||
}
|
||||
|
||||
declare namespace HttpRequestNode {
|
||||
namespace V2 {
|
||||
type AuthParams = {
|
||||
authentication: 'none' | 'genericCredentialType' | 'predefinedCredentialType';
|
||||
genericAuthType: string;
|
||||
nodeCredentialType: string;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectLiteral(maybeObject: unknown): maybeObject is { [key: string]: string } {
|
||||
return typeof maybeObject === 'object' && maybeObject !== null && !Array.isArray(maybeObject);
|
||||
}
|
||||
|
||||
@@ -27,8 +27,13 @@ export const showMessage = mixins(externalHooks).extend({
|
||||
stickyNotificationQueue.push(notification);
|
||||
}
|
||||
|
||||
if(messageData.type === 'error' && track) {
|
||||
this.$telemetry.track('Instance FE emitted error', { error_title: messageData.title, error_message: messageData.message, workflow_id: this.$store.getters.workflowId });
|
||||
if (messageData.type === 'error' && track) {
|
||||
this.$telemetry.track('Instance FE emitted error', {
|
||||
error_title: messageData.title,
|
||||
error_message: messageData.message,
|
||||
caused_by_credential: this.causedByCredential(messageData.message),
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
});
|
||||
}
|
||||
|
||||
return notification;
|
||||
@@ -135,7 +140,14 @@ export const showMessage = mixins(externalHooks).extend({
|
||||
message,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
this.$telemetry.track('Instance FE emitted error', { error_title: title, error_description: message, error_message: error.message, workflow_id: this.$store.getters.workflowId });
|
||||
|
||||
this.$telemetry.track('Instance FE emitted error', {
|
||||
error_title: title,
|
||||
error_description: message,
|
||||
error_message: error.message,
|
||||
caused_by_credential: this.causedByCredential(error.message),
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
});
|
||||
},
|
||||
|
||||
async confirmMessage (message: string, headline: string, type: MessageType | null = 'warning', confirmButtonText?: string, cancelButtonText?: string): Promise<boolean> {
|
||||
@@ -203,5 +215,14 @@ export const showMessage = mixins(externalHooks).extend({
|
||||
</details>
|
||||
`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether a workflow execution error was caused by a credential issue, as reflected by the error message.
|
||||
*/
|
||||
causedByCredential(message: string | undefined) {
|
||||
if (!message) return false;
|
||||
|
||||
return message.includes('Credentials for') && message.includes('are not set');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -330,6 +330,11 @@ export const workflowHelpers = mixins(
|
||||
if (node.credentials !== undefined && nodeType.credentials !== undefined) {
|
||||
const saveCredenetials: INodeCredentials = {};
|
||||
for (const nodeCredentialTypeName of Object.keys(node.credentials)) {
|
||||
if (this.hasProxyAuth(node) || Object.keys(node.parameters).includes('genericAuthType')) {
|
||||
saveCredenetials[nodeCredentialTypeName] = node.credentials[nodeCredentialTypeName];
|
||||
continue;
|
||||
}
|
||||
|
||||
const credentialTypeDescription = nodeType.credentials
|
||||
.find((credentialTypeDescription) => credentialTypeDescription.name === nodeCredentialTypeName);
|
||||
|
||||
|
||||
@@ -4,6 +4,9 @@ export const NODE_NAME_PREFIX = 'node-';
|
||||
|
||||
export const PLACEHOLDER_FILLED_AT_EXECUTION_TIME = '[filled at execution time]';
|
||||
|
||||
// parameter input
|
||||
export const CUSTOM_API_CALL_KEY = '__CUSTOM_API_CALL__';
|
||||
|
||||
// workflows
|
||||
export const PLACEHOLDER_EMPTY_WORKFLOW_ID = '__EMPTY__';
|
||||
export const DEFAULT_NODETYPE_VERSION = 1;
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
ICredentialsDecrypted,
|
||||
INodeCredentialTestResult,
|
||||
INodeTypeDescription,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { getAppNameFromCredType } from '@/components/helpers';
|
||||
|
||||
@@ -120,6 +121,35 @@ const module: Module<ICredentialsState, IRootState> = {
|
||||
});
|
||||
};
|
||||
},
|
||||
getScopesByCredentialType (_: ICredentialsState, getters: any) { // tslint:disable-line:no-any
|
||||
return (credentialTypeName: string) => {
|
||||
const credentialType = getters.getCredentialTypeByName(credentialTypeName) as {
|
||||
properties: INodeProperties[];
|
||||
};
|
||||
|
||||
const scopeProperty = credentialType.properties.find((p) => p.name === 'scope');
|
||||
|
||||
if (
|
||||
!scopeProperty ||
|
||||
!scopeProperty.default ||
|
||||
typeof scopeProperty.default !== 'string' ||
|
||||
scopeProperty.default === ''
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let { default: scopeDefault } = scopeProperty;
|
||||
|
||||
// disregard expressions for display
|
||||
scopeDefault = scopeDefault.replace(/^=/, '').replace(/\{\{.*\}\}/, '');
|
||||
|
||||
if (/ /.test(scopeDefault)) return scopeDefault.split(' ');
|
||||
|
||||
if (/,/.test(scopeDefault)) return scopeDefault.split(',');
|
||||
|
||||
return [scopeDefault];
|
||||
};
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
fetchCredentialTypes: async (context: ActionContext<ICredentialsState, IRootState>) => {
|
||||
|
||||
@@ -318,8 +318,8 @@ Allowed subkeys: `options.{optionName}.displayName` and `options.{optionName}.de
|
||||
{
|
||||
"nodeView.resource.displayName": "🇩🇪 Resource",
|
||||
"nodeView.resource.description": "🇩🇪 Resource to operate on",
|
||||
"nodeView.resource.options.file.displayName": "🇩🇪 File",
|
||||
"nodeView.resource.options.issue.displayName": "🇩🇪 Issue",
|
||||
"nodeView.resource.options.file.name": "🇩🇪 File",
|
||||
"nodeView.resource.options.issue.name": "🇩🇪 Issue",
|
||||
}
|
||||
```
|
||||
|
||||
@@ -327,6 +327,16 @@ Allowed subkeys: `options.{optionName}.displayName` and `options.{optionName}.de
|
||||
<img src="img/node2.png" width="400">
|
||||
</p>
|
||||
|
||||
For nodes whose credentials may be used in the HTTP Request node, an additional option `Custom API Call` is injected into the `Resource` and `Operation` parameters. Use the `__CUSTOM_API_CALL__` key to translate this additional option.
|
||||
|
||||
```json
|
||||
{
|
||||
"nodeView.resource.options.file.name": "🇩🇪 File",
|
||||
"nodeView.resource.options.issue.name": "🇩🇪 Issue",
|
||||
"nodeView.resource.options.__CUSTOM_API_CALL__.name": "🇩🇪 Custom API Call",
|
||||
}
|
||||
```
|
||||
|
||||
#### `collection` and `fixedCollection` parameters
|
||||
|
||||
Allowed keys: `displayName`, `description`, `placeholder`, `multipleValueButtonText`
|
||||
|
||||
@@ -242,6 +242,8 @@
|
||||
"forgotPassword.returnToSignIn": "Back to sign in",
|
||||
"forgotPassword.sendingEmailError": "Problem sending email",
|
||||
"forgotPassword.smtpErrorContactAdministrator": "Please contact your administrator (problem with your SMTP setup)",
|
||||
"generic.oauth1Api": "OAuth1 API",
|
||||
"generic.oauth2Api": "OAuth2 API",
|
||||
"genericHelpers.loading": "Loading",
|
||||
"genericHelpers.min": "min",
|
||||
"genericHelpers.sec": "sec",
|
||||
@@ -402,6 +404,7 @@
|
||||
"nodeErrorView.stack": "Stack",
|
||||
"nodeErrorView.theErrorCauseIsTooLargeToBeDisplayed": "The error cause is too large to be displayed",
|
||||
"nodeErrorView.time": "Time",
|
||||
"nodeHelpers.credentialsUnset": "Credentials for '{credentialType}' are not set.",
|
||||
"nodeSettings.alwaysOutputData.description": "If active, will output a single, empty item when the output would have been empty. Use to prevent the workflow finishing on this node.",
|
||||
"nodeSettings.alwaysOutputData.displayName": "Always Output Data",
|
||||
"nodeSettings.clickOnTheQuestionMarkIcon": "Click the '?' icon to open this node on n8n.io",
|
||||
@@ -421,8 +424,11 @@
|
||||
"nodeSettings.parameters": "Parameters",
|
||||
"nodeSettings.retryOnFail.description": "If active, the node tries to execute again when it fails",
|
||||
"nodeSettings.retryOnFail.displayName": "Retry On Fail",
|
||||
"nodeSettings.scopes.expandedNoticeWithScopes": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a> | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials<br>{scopes}<br><a data-key=\"show-less\">Show less</a>",
|
||||
"nodeSettings.scopes.notice": "<a data-key=\"toggle-expand\">{count} scope</a> available for {activeCredential} credentials | <a data-key=\"toggle-expand\">{count} scopes</a> available for {activeCredential} credentials",
|
||||
"nodeSettings.theNodeIsNotValidAsItsTypeIsUnknown": "The node is not valid as its type ({nodeType}) is unknown",
|
||||
"nodeSettings.thisNodeDoesNotHaveAnyParameters": "This node does not have any parameters",
|
||||
"nodeSettings.useTheHttpRequestNode": "Use the <b>HTTP Request</b> node to make a custom API call. We'll take care of the {nodeTypeDisplayName} auth for you. <a target=\"_blank\" href=\"https://docs.n8n.io/integrations/custom-operations/\">Learn more</a>",
|
||||
"nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)",
|
||||
"nodeSettings.waitBetweenTries.displayName": "Wait Between Tries (ms)",
|
||||
"nodeView.addNode": "Add node",
|
||||
@@ -499,6 +505,7 @@
|
||||
"openWorkflow.workflowImportError": "Could not import workflow",
|
||||
"openWorkflow.workflowNotFoundError": "Could not find workflow",
|
||||
"parameterInput.addExpression": "Add Expression",
|
||||
"parameterInput.customApiCall": "Custom API Call",
|
||||
"parameterInput.error": "ERROR",
|
||||
"parameterInput.issues": "Issues",
|
||||
"parameterInput.loadingOptions": "Loading options...",
|
||||
@@ -513,6 +520,8 @@
|
||||
"parameterInput.resetValue": "Reset Value",
|
||||
"parameterInput.select": "Select",
|
||||
"parameterInput.selectDateAndTime": "Select date and time",
|
||||
"parameterInput.selectACredentialTypeFromTheDropdown": "Select a credential type from the dropdown",
|
||||
"parameterInput.theValueIsNotSupported": "The value \"{checkValue}\" is not supported!",
|
||||
"parameterInputExpanded.openDocs": "Open docs",
|
||||
"parameterInputExpanded.thisFieldIsRequired": "This field is required",
|
||||
"parameterInputList.delete": "Delete",
|
||||
|
||||
Reference in New Issue
Block a user