## Summary Provide details about your pull request and what it adds, fixes, or changes. Photos and videos are recommended. As part of NodeView refactor, this PR migrates all externalHooks calls to `useExternalHooks` composable. #### How to test the change: 1. Run using env `export N8N_DEPLOYMENT_TYPE=cloud` 2. Hooks should still run as expected ## Issues fixed Include links to Github issue or Community forum post or **Linear ticket**: > Important in order to close automatically and provide context to reviewers https://linear.app/n8n/issue/N8N-6349/externalhooks ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. A feature is not complete without tests. > > *(internal)* You can use Slack commands to trigger [e2e tests](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#a39f9e5ba64a48b58a71d81c837e8227) or [deploy test instance](https://www.notion.so/n8n/How-to-use-Test-Instances-d65f49dfc51f441ea44367fb6f67eb0a?pvs=4#f6a177d32bde4b57ae2da0b8e454bfce) or [deploy early access version on Cloud](https://www.notion.so/n8n/Cloudbot-3dbe779836004972b7057bc989526998?pvs=4#fef2d36ab02247e1a0f65a74f6fb534e).
1414 lines
39 KiB
Vue
1414 lines
39 KiB
Vue
<template>
|
|
<div @keydown.stop :class="parameterInputClasses">
|
|
<expression-edit
|
|
:dialogVisible="expressionEditDialogVisible"
|
|
:modelValue="
|
|
isResourceLocatorParameter && typeof modelValue !== 'string'
|
|
? modelValue
|
|
? modelValue.value
|
|
: ''
|
|
: modelValue
|
|
"
|
|
:parameter="parameter"
|
|
:path="path"
|
|
:eventSource="eventSource || 'ndv'"
|
|
:isReadOnly="isReadOnly"
|
|
:redactValues="shouldRedactValue"
|
|
@closeDialog="closeExpressionEditDialog"
|
|
@update:modelValue="expressionUpdated"
|
|
></expression-edit>
|
|
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle">
|
|
<resource-locator
|
|
v-if="isResourceLocatorParameter"
|
|
ref="resourceLocator"
|
|
:parameter="parameter"
|
|
:modelValue="modelValue"
|
|
:dependentParametersValues="dependentParametersValues"
|
|
:displayTitle="displayTitle"
|
|
:expressionDisplayValue="expressionDisplayValue"
|
|
:expressionComputedValue="expressionEvaluated"
|
|
:isValueExpression="isValueExpression"
|
|
:isReadOnly="isReadOnly"
|
|
:parameterIssues="getIssues"
|
|
:droppable="droppable"
|
|
:node="node"
|
|
:path="path"
|
|
:event-bus="eventBus"
|
|
@update:modelValue="valueChanged"
|
|
@modalOpenerClick="openExpressionEditorModal"
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
@drop="onResourceLocatorDrop"
|
|
/>
|
|
<ExpressionParameterInput
|
|
v-else-if="isValueExpression || forceShowExpression"
|
|
:modelValue="expressionDisplayValue"
|
|
:title="displayTitle"
|
|
:isReadOnly="isReadOnly"
|
|
:path="path"
|
|
:additional-expression-data="additionalExpressionData"
|
|
:class="{ 'ph-no-capture': shouldRedactValue }"
|
|
@update:modelValue="expressionUpdated"
|
|
@modalOpenerClick="openExpressionEditorModal"
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
ref="inputField"
|
|
/>
|
|
<div
|
|
v-else-if="
|
|
['json', 'string'].includes(parameter.type) ||
|
|
remoteParameterOptionsLoadingIssues !== null
|
|
"
|
|
>
|
|
<el-dialog
|
|
v-if="codeEditDialogVisible"
|
|
:modelValue="true"
|
|
append-to-body
|
|
:close-on-click-modal="false"
|
|
width="80%"
|
|
:title="`${i18n.baseText('codeEdit.edit')} ${$locale
|
|
.nodeText()
|
|
.inputLabelDisplayName(parameter, path)}`"
|
|
:before-close="closeCodeEditDialog"
|
|
>
|
|
<div class="ignore-key-press">
|
|
<code-node-editor
|
|
:modelValue="modelValue"
|
|
:defaultValue="parameter.default"
|
|
:language="editorLanguage"
|
|
:isReadOnly="isReadOnly"
|
|
@update:modelValue="expressionUpdated"
|
|
/>
|
|
</div>
|
|
</el-dialog>
|
|
|
|
<text-edit
|
|
:dialogVisible="textEditDialogVisible"
|
|
:modelValue="modelValue"
|
|
:parameter="parameter"
|
|
:path="path"
|
|
:isReadOnly="isReadOnly"
|
|
@closeDialog="closeTextEditDialog"
|
|
@update:modelValue="expressionUpdated"
|
|
></text-edit>
|
|
|
|
<code-node-editor
|
|
v-if="editorType === 'codeNodeEditor' && isCodeNode(node)"
|
|
:mode="node.parameters.mode"
|
|
:modelValue="modelValue"
|
|
:defaultValue="parameter.default"
|
|
:language="editorLanguage"
|
|
:isReadOnly="isReadOnly"
|
|
:rows="getArgument('rows')"
|
|
:aiButtonEnabled="settingsStore.isCloudDeployment"
|
|
@update:modelValue="valueChangedDebounced"
|
|
/>
|
|
|
|
<html-editor
|
|
v-else-if="editorType === 'htmlEditor'"
|
|
:modelValue="modelValue"
|
|
:isReadOnly="isReadOnly"
|
|
:rows="getArgument('rows')"
|
|
:disableExpressionColoring="!isHtmlNode(node)"
|
|
:disableExpressionCompletions="!isHtmlNode(node)"
|
|
@update:modelValue="valueChangedDebounced"
|
|
/>
|
|
|
|
<sql-editor
|
|
v-else-if="editorType === 'sqlEditor'"
|
|
:modelValue="modelValue"
|
|
:dialect="getArgument('sqlDialect')"
|
|
:isReadOnly="isReadOnly"
|
|
:rows="getArgument('rows')"
|
|
@update:modelValue="valueChangedDebounced"
|
|
/>
|
|
|
|
<code-node-editor
|
|
v-else-if="editorType === 'json' && !isExecuteWorkflowNode(node)"
|
|
:mode="node.parameters.mode"
|
|
:modelValue="modelValue"
|
|
:defaultValue="parameter.default"
|
|
:language="editorLanguage"
|
|
:isReadOnly="isReadOnly"
|
|
:aiButtonEnabled="false"
|
|
@update:modelValue="valueChangedDebounced"
|
|
:rows="getArgument('rows')"
|
|
/>
|
|
|
|
<div v-else-if="editorType" class="readonly-code clickable" @click="displayEditDialog()">
|
|
<code-node-editor
|
|
v-if="!codeEditDialogVisible"
|
|
:modelValue="modelValue"
|
|
:language="editorLanguage"
|
|
:isReadOnly="true"
|
|
:rows="getArgument('rows')"
|
|
/>
|
|
</div>
|
|
|
|
<n8n-input
|
|
v-else
|
|
v-model="tempValue"
|
|
ref="inputField"
|
|
:class="{ 'input-with-opener': true, 'ph-no-capture': shouldRedactValue }"
|
|
:size="inputSize"
|
|
:type="getStringInputType"
|
|
:rows="getArgument('rows')"
|
|
:disabled="isReadOnly"
|
|
@update:modelValue="valueChanged($event) && onUpdateTextInput($event)"
|
|
@keydown.stop
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
:title="displayTitle"
|
|
:placeholder="getPlaceholder()"
|
|
>
|
|
<template #suffix>
|
|
<n8n-icon
|
|
v-if="!isReadOnly && !isSecretParameter"
|
|
icon="external-link-alt"
|
|
size="xsmall"
|
|
class="edit-window-button textarea-modal-opener"
|
|
:class="{
|
|
focused: isFocused,
|
|
invalid: !isFocused && getIssues.length > 0 && !isValueExpression,
|
|
}"
|
|
:title="i18n.baseText('parameterInput.openEditWindow')"
|
|
@click="displayEditDialog()"
|
|
@focus="setFocus"
|
|
/>
|
|
</template>
|
|
</n8n-input>
|
|
</div>
|
|
|
|
<div v-else-if="parameter.type === 'color'" ref="inputField" class="color-input">
|
|
<el-color-picker
|
|
size="small"
|
|
class="color-picker"
|
|
:modelValue="displayValue"
|
|
:disabled="isReadOnly"
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
@update:modelValue="valueChanged"
|
|
:title="displayTitle"
|
|
:show-alpha="getArgument('showAlpha')"
|
|
/>
|
|
<n8n-input
|
|
v-model="tempValue"
|
|
:size="inputSize"
|
|
type="text"
|
|
:disabled="isReadOnly"
|
|
@update:modelValue="valueChanged"
|
|
@keydown.stop
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
:title="displayTitle"
|
|
/>
|
|
</div>
|
|
|
|
<el-date-picker
|
|
v-else-if="parameter.type === 'dateTime'"
|
|
v-model="tempValue"
|
|
ref="inputField"
|
|
type="datetime"
|
|
:size="inputSize"
|
|
:modelValue="displayValue"
|
|
:title="displayTitle"
|
|
:disabled="isReadOnly"
|
|
:placeholder="
|
|
parameter.placeholder
|
|
? getPlaceholder()
|
|
: i18n.baseText('parameterInput.selectDateAndTime')
|
|
"
|
|
:picker-options="dateTimePickerOptions"
|
|
:class="{ 'ph-no-capture': shouldRedactValue }"
|
|
@update:modelValue="valueChanged"
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
@keydown.stop
|
|
/>
|
|
|
|
<n8n-input-number
|
|
v-else-if="parameter.type === 'number'"
|
|
ref="inputField"
|
|
:size="inputSize"
|
|
:modelValue="displayValue"
|
|
:controls="false"
|
|
:max="getArgument('maxValue')"
|
|
:min="getArgument('minValue')"
|
|
:precision="getArgument('numberPrecision')"
|
|
:disabled="isReadOnly"
|
|
:class="{ 'ph-no-capture': shouldRedactValue }"
|
|
@update:modelValue="onUpdateTextInput"
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
@keydown.stop
|
|
:title="displayTitle"
|
|
: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"
|
|
@update:modelValue="valueChanged"
|
|
@setFocus="setFocus"
|
|
@onBlur="onBlur"
|
|
>
|
|
<template #issues-and-options>
|
|
<parameter-issues :issues="getIssues" />
|
|
</template>
|
|
</credentials-select>
|
|
|
|
<n8n-select
|
|
v-else-if="parameter.type === 'options'"
|
|
ref="inputField"
|
|
:size="inputSize"
|
|
filterable
|
|
:modelValue="displayValue"
|
|
:placeholder="
|
|
parameter.placeholder ? getPlaceholder() : i18n.baseText('parameterInput.select')
|
|
"
|
|
:loading="remoteParameterOptionsLoading"
|
|
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
|
:title="displayTitle"
|
|
@update:modelValue="valueChanged"
|
|
@keydown.stop
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
>
|
|
<n8n-option
|
|
v-for="option in parameterOptions"
|
|
:value="option.value"
|
|
:key="option.value"
|
|
:label="getOptionsOptionDisplayName(option)"
|
|
>
|
|
<div class="list-option">
|
|
<div
|
|
class="option-headline"
|
|
:class="{ 'remote-parameter-option': isRemoteParameterOption(option) }"
|
|
>
|
|
{{ getOptionsOptionDisplayName(option) }}
|
|
</div>
|
|
<div
|
|
v-if="option.description"
|
|
class="option-description"
|
|
v-html="getOptionsOptionDescription(option)"
|
|
></div>
|
|
</div>
|
|
</n8n-option>
|
|
</n8n-select>
|
|
|
|
<n8n-select
|
|
v-else-if="parameter.type === 'multiOptions'"
|
|
ref="inputField"
|
|
:size="inputSize"
|
|
filterable
|
|
multiple
|
|
:modelValue="displayValue"
|
|
:loading="remoteParameterOptionsLoading"
|
|
:disabled="isReadOnly || remoteParameterOptionsLoading"
|
|
:title="displayTitle"
|
|
:placeholder="i18n.baseText('parameterInput.select')"
|
|
@update:modelValue="valueChanged"
|
|
@keydown.stop
|
|
@focus="setFocus"
|
|
@blur="onBlur"
|
|
>
|
|
<n8n-option
|
|
v-for="option in parameterOptions"
|
|
:value="option.value"
|
|
:key="option.value"
|
|
:label="getOptionsOptionDisplayName(option)"
|
|
>
|
|
<div class="list-option">
|
|
<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>
|
|
|
|
<!-- temporary state of booleans while data is mapped -->
|
|
<n8n-input
|
|
v-else-if="parameter.type === 'boolean' && droppable"
|
|
:size="inputSize"
|
|
:modelValue="JSON.stringify(displayValue)"
|
|
:disabled="isReadOnly"
|
|
:title="displayTitle"
|
|
/>
|
|
<el-switch
|
|
v-else-if="parameter.type === 'boolean'"
|
|
:class="{ 'switch-input': true, 'ph-no-capture': shouldRedactValue }"
|
|
ref="inputField"
|
|
active-color="#13ce66"
|
|
:modelValue="displayValue"
|
|
:disabled="isReadOnly"
|
|
@update:modelValue="valueChanged"
|
|
/>
|
|
</div>
|
|
|
|
<parameter-issues
|
|
v-if="parameter.type !== 'credentialsSelect' && !isResourceLocatorParameter"
|
|
:issues="getIssues"
|
|
/>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
/* eslint-disable prefer-spread */
|
|
import { defineComponent } from 'vue';
|
|
import { mapStores } from 'pinia';
|
|
|
|
import { get } from 'lodash-es';
|
|
|
|
import type { INodeUi, INodeUpdatePropertiesInformation } from '@/Interface';
|
|
import type {
|
|
ILoadOptions,
|
|
INodeParameters,
|
|
INodePropertyOptions,
|
|
Workflow,
|
|
INodeProperties,
|
|
INodePropertyCollection,
|
|
NodeParameterValueType,
|
|
IParameterLabel,
|
|
EditorType,
|
|
CodeNodeEditorLanguage,
|
|
IDataObject,
|
|
} from 'n8n-workflow';
|
|
import { NodeHelpers, CREDENTIAL_EMPTY_VALUE } from 'n8n-workflow';
|
|
|
|
import CredentialsSelect from '@/components/CredentialsSelect.vue';
|
|
import ExpressionEdit from '@/components/ExpressionEdit.vue';
|
|
import ParameterIssues from '@/components/ParameterIssues.vue';
|
|
import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue';
|
|
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
|
|
import TextEdit from '@/components/TextEdit.vue';
|
|
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
|
|
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
|
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
|
|
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
|
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
|
|
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
|
|
|
import {
|
|
CUSTOM_API_CALL_KEY,
|
|
EXECUTE_WORKFLOW_NODE_TYPE,
|
|
HTML_NODE_TYPE,
|
|
NODES_USING_CODE_NODE_EDITOR,
|
|
} from '@/constants';
|
|
|
|
import type { PropType } from 'vue';
|
|
import { debounceHelper } from '@/mixins/debounce';
|
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
|
import { useCredentialsStore } from '@/stores/credentials.store';
|
|
import { useSettingsStore } from '@/stores/settings.store';
|
|
import { htmlEditorEventBus } from '@/event-bus';
|
|
import type { EventBus } from 'n8n-design-system/utils';
|
|
import { createEventBus } from 'n8n-design-system/utils';
|
|
import { useI18n } from '@/composables/useI18n';
|
|
import type { N8nInput } from 'n8n-design-system';
|
|
import { isCredentialOnlyNodeType } from '@/utils/credentialOnlyNodes';
|
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
|
|
|
type Picker = { $emit: (arg0: string, arg1: Date) => void };
|
|
|
|
export default defineComponent({
|
|
name: 'parameter-input',
|
|
mixins: [nodeHelpers, workflowHelpers, debounceHelper],
|
|
components: {
|
|
CodeNodeEditor,
|
|
HtmlEditor,
|
|
SqlEditor,
|
|
ExpressionEdit,
|
|
ExpressionParameterInput,
|
|
CredentialsSelect,
|
|
ParameterIssues,
|
|
ResourceLocator,
|
|
TextEdit,
|
|
},
|
|
props: {
|
|
additionalExpressionData: {
|
|
type: Object as PropType<IDataObject>,
|
|
default: () => ({}),
|
|
},
|
|
isReadOnly: {
|
|
type: Boolean,
|
|
},
|
|
parameter: {
|
|
type: Object as PropType<INodeProperties>,
|
|
},
|
|
path: {
|
|
type: String,
|
|
},
|
|
modelValue: {
|
|
type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>,
|
|
},
|
|
hideLabel: {
|
|
type: Boolean,
|
|
},
|
|
droppable: {
|
|
type: Boolean,
|
|
},
|
|
activeDrop: {
|
|
type: Boolean,
|
|
},
|
|
forceShowExpression: {
|
|
type: Boolean,
|
|
},
|
|
hint: {
|
|
type: String as PropType<string | undefined>,
|
|
},
|
|
inputSize: {
|
|
type: String,
|
|
},
|
|
hideIssues: {
|
|
type: Boolean,
|
|
},
|
|
documentationUrl: {
|
|
type: String as PropType<string | undefined>,
|
|
},
|
|
errorHighlight: {
|
|
type: Boolean,
|
|
},
|
|
isForCredential: {
|
|
type: Boolean,
|
|
},
|
|
eventSource: {
|
|
type: String,
|
|
},
|
|
expressionEvaluated: {
|
|
type: String as PropType<string | undefined>,
|
|
},
|
|
label: {
|
|
type: Object as PropType<IParameterLabel>,
|
|
default: () => ({
|
|
size: 'small',
|
|
}),
|
|
},
|
|
eventBus: {
|
|
type: Object as PropType<EventBus>,
|
|
default: () => createEventBus(),
|
|
},
|
|
},
|
|
setup() {
|
|
const externalHooks = useExternalHooks();
|
|
const i18n = useI18n();
|
|
|
|
return {
|
|
externalHooks,
|
|
i18n,
|
|
};
|
|
},
|
|
data() {
|
|
return {
|
|
codeEditDialogVisible: false,
|
|
nodeName: '',
|
|
expressionAddOperation: 'set' as 'add' | 'set',
|
|
expressionEditDialogVisible: false,
|
|
remoteParameterOptions: [] as INodePropertyOptions[],
|
|
remoteParameterOptionsLoading: false,
|
|
remoteParameterOptionsLoadingIssues: null as string | null,
|
|
textEditDialogVisible: false,
|
|
editDialogClosing: 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: [
|
|
{
|
|
text: 'Today', // TODO
|
|
|
|
onClick(picker: Picker) {
|
|
picker.$emit('pick', new Date());
|
|
},
|
|
},
|
|
{
|
|
text: 'Yesterday', // TODO
|
|
|
|
onClick(picker: Picker) {
|
|
const date = new Date();
|
|
date.setTime(date.getTime() - 3600 * 1000 * 24);
|
|
picker.$emit('pick', date);
|
|
},
|
|
},
|
|
{
|
|
text: 'A week ago', // TODO
|
|
|
|
onClick(picker: Picker) {
|
|
const date = new Date();
|
|
date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
|
|
picker.$emit('pick', date);
|
|
},
|
|
},
|
|
],
|
|
},
|
|
isFocused: false,
|
|
};
|
|
},
|
|
watch: {
|
|
async dependentParametersValues() {
|
|
// Reload the remote parameters whenever a parameter
|
|
// on which the current field depends on changes
|
|
await this.loadRemoteParameterOptions();
|
|
},
|
|
modelValue() {
|
|
if (this.parameter.type === 'color' && this.getArgument('showAlpha') === true) {
|
|
// Do not set for color with alpha else wrong value gets displayed in field
|
|
return;
|
|
}
|
|
this.tempValue = this.displayValue as string;
|
|
},
|
|
},
|
|
computed: {
|
|
...mapStores(
|
|
useCredentialsStore,
|
|
useNodeTypesStore,
|
|
useNDVStore,
|
|
useWorkflowsStore,
|
|
useSettingsStore,
|
|
),
|
|
expressionDisplayValue(): string {
|
|
if (this.forceShowExpression) {
|
|
return '';
|
|
}
|
|
|
|
const value = isResourceLocatorValue(this.modelValue)
|
|
? this.modelValue.value
|
|
: this.modelValue;
|
|
if (typeof value === 'string' && value.startsWith('=')) {
|
|
return value.slice(1);
|
|
}
|
|
|
|
return `${this.displayValue ?? ''}`;
|
|
},
|
|
isValueExpression(): boolean {
|
|
return isValueExpression(this.parameter, this.modelValue);
|
|
},
|
|
codeAutocomplete(): string | undefined {
|
|
return this.getArgument('codeAutocomplete') as string | undefined;
|
|
},
|
|
dependentParametersValues(): string | null {
|
|
const loadOptionsDependsOn = this.getArgument('loadOptionsDependsOn') as string[] | undefined;
|
|
|
|
if (loadOptionsDependsOn === undefined) {
|
|
return null;
|
|
}
|
|
|
|
// Get the resolved parameter values of the current node
|
|
const currentNodeParameters = this.ndvStore.activeNode?.parameters;
|
|
try {
|
|
const resolvedNodeParameters = this.resolveParameter(currentNodeParameters);
|
|
|
|
const returnValues: string[] = [];
|
|
for (const parameterPath of loadOptionsDependsOn) {
|
|
returnValues.push(get(resolvedNodeParameters, parameterPath) as string);
|
|
}
|
|
|
|
return returnValues.join('|');
|
|
} catch (error) {
|
|
return null;
|
|
}
|
|
},
|
|
node(): INodeUi {
|
|
return this.ndvStore.activeNode!;
|
|
},
|
|
displayTitle(): string {
|
|
const interpolation = { interpolate: { shortPath: this.shortPath } };
|
|
|
|
if (this.getIssues.length && this.isValueExpression) {
|
|
return this.i18n.baseText('parameterInput.parameterHasIssuesAndExpression', interpolation);
|
|
} else if (this.getIssues.length && !this.isValueExpression) {
|
|
return this.i18n.baseText('parameterInput.parameterHasIssues', interpolation);
|
|
} else if (!this.getIssues.length && this.isValueExpression) {
|
|
return this.i18n.baseText('parameterInput.parameterHasExpression', interpolation);
|
|
}
|
|
|
|
return this.i18n.baseText('parameterInput.parameter', interpolation);
|
|
},
|
|
displayValue(): string | number | boolean | null {
|
|
if (this.remoteParameterOptionsLoading) {
|
|
// If it is loading options from server display
|
|
// to user that the data is loading. If not it would
|
|
// display the user the key instead of the value it
|
|
// represents
|
|
return this.i18n.baseText('parameterInput.loadingOptions');
|
|
}
|
|
|
|
// if the value is marked as empty return empty string, to prevent displaying the asterisks
|
|
if (this.modelValue === CREDENTIAL_EMPTY_VALUE) {
|
|
return '';
|
|
}
|
|
|
|
let returnValue;
|
|
if (!this.isValueExpression) {
|
|
returnValue = this.isResourceLocatorParameter
|
|
? isResourceLocatorValue(this.modelValue)
|
|
? this.modelValue.value
|
|
: ''
|
|
: this.modelValue;
|
|
} else {
|
|
returnValue = this.expressionEvaluated;
|
|
}
|
|
|
|
if (this.parameter.type === 'credentialsSelect' && typeof this.modelValue === 'string') {
|
|
const credType = this.credentialsStore.getCredentialTypeByName(this.modelValue);
|
|
if (credType) {
|
|
returnValue = credType.displayName;
|
|
}
|
|
}
|
|
|
|
if (
|
|
Array.isArray(returnValue) &&
|
|
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);
|
|
const h = [];
|
|
h.push((bigint >> 24) & 255);
|
|
h.push((bigint >> 16) & 255);
|
|
h.push((bigint >> 8) & 255);
|
|
h.push(((255 - bigint) & 255) / 255);
|
|
|
|
returnValue = 'rgba(' + h.join() + ')';
|
|
}
|
|
|
|
if (returnValue !== undefined && returnValue !== null && this.parameter.type === 'string') {
|
|
const rows = this.getArgument('rows');
|
|
if (rows === undefined || rows === 1) {
|
|
returnValue = returnValue.toString().replace(/\n/, '|');
|
|
}
|
|
}
|
|
|
|
return returnValue;
|
|
},
|
|
getStringInputType() {
|
|
if (this.getArgument('password') === true) {
|
|
return 'password';
|
|
}
|
|
|
|
const rows = this.getArgument('rows');
|
|
if (rows !== undefined && rows > 1) {
|
|
return 'textarea';
|
|
}
|
|
|
|
if (this.editorType === 'code') {
|
|
return 'textarea';
|
|
}
|
|
|
|
return 'text';
|
|
},
|
|
getIssues(): string[] {
|
|
if (this.hideIssues || this.node === null) {
|
|
return [];
|
|
}
|
|
|
|
const newPath = this.shortPath.split('.');
|
|
newPath.pop();
|
|
|
|
const issues = NodeHelpers.getParameterIssues(
|
|
this.parameter,
|
|
this.node.parameters,
|
|
newPath.join('.'),
|
|
this.node,
|
|
);
|
|
|
|
if (this.parameter.type === 'credentialsSelect' && this.displayValue === '') {
|
|
issues.parameters = issues.parameters || {};
|
|
|
|
const issue = this.i18n.baseText('parameterInput.selectACredentialTypeFromTheDropdown');
|
|
|
|
issues.parameters[this.parameter.name] = [issue];
|
|
} else if (
|
|
['options', 'multiOptions'].includes(this.parameter.type) &&
|
|
!this.remoteParameterOptionsLoading &&
|
|
this.remoteParameterOptionsLoadingIssues === null &&
|
|
this.parameterOptions
|
|
) {
|
|
// 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
|
|
// and the error is not displayed on the node in the workflow
|
|
const validOptions = this.parameterOptions.map(
|
|
(options) => (options as INodePropertyOptions).value,
|
|
);
|
|
|
|
const checkValues: 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 === null || !validOptions.includes(checkValue)) {
|
|
if (issues.parameters === undefined) {
|
|
issues.parameters = {};
|
|
}
|
|
|
|
const issue = this.i18n.baseText('parameterInput.theValueIsNotSupported', {
|
|
interpolate: { checkValue },
|
|
});
|
|
|
|
issues.parameters[this.parameter.name] = [issue];
|
|
}
|
|
}
|
|
} else if (this.remoteParameterOptionsLoadingIssues !== null && !this.isValueExpression) {
|
|
if (issues.parameters === undefined) {
|
|
issues.parameters = {};
|
|
}
|
|
issues.parameters[this.parameter.name] = [
|
|
`There was a problem loading the parameter options from server: "${this.remoteParameterOptionsLoadingIssues}"`,
|
|
];
|
|
}
|
|
|
|
if (issues?.parameters?.[this.parameter.name] !== undefined) {
|
|
return issues.parameters[this.parameter.name];
|
|
}
|
|
|
|
return [];
|
|
},
|
|
editorType(): EditorType {
|
|
return this.getArgument('editor') as EditorType;
|
|
},
|
|
editorLanguage(): CodeNodeEditorLanguage {
|
|
if (this.editorType === 'json' || this.parameter.type === 'json') return 'json';
|
|
return (this.getArgument('editorLanguage') as CodeNodeEditorLanguage) ?? 'javaScript';
|
|
},
|
|
parameterOptions():
|
|
| Array<INodePropertyOptions | INodeProperties | INodePropertyCollection>
|
|
| undefined {
|
|
if (!this.hasRemoteMethod) {
|
|
// Options are already given
|
|
return this.parameter.options;
|
|
}
|
|
|
|
// Options get loaded from server
|
|
return this.remoteParameterOptions;
|
|
},
|
|
parameterInputClasses() {
|
|
const classes: { [c: string]: boolean } = {
|
|
droppable: this.droppable,
|
|
activeDrop: this.activeDrop,
|
|
};
|
|
|
|
const rows = this.getArgument('rows');
|
|
const isTextarea = this.parameter.type === 'string' && rows !== undefined;
|
|
const isSwitch = this.parameter.type === 'boolean' && !this.isValueExpression;
|
|
|
|
if (!isTextarea && !isSwitch) {
|
|
classes['parameter-value-container'] = true;
|
|
}
|
|
|
|
if (
|
|
!this.droppable &&
|
|
!this.activeDrop &&
|
|
(this.getIssues.length > 0 || this.errorHighlight) &&
|
|
!this.isValueExpression
|
|
) {
|
|
classes['has-issues'] = true;
|
|
}
|
|
|
|
return classes;
|
|
},
|
|
parameterInputWrapperStyle() {
|
|
let deductWidth = 0;
|
|
const styles = {
|
|
width: '100%',
|
|
};
|
|
if (this.parameter.type === 'credentialsSelect' || this.isResourceLocatorParameter) {
|
|
return styles;
|
|
}
|
|
if (this.getIssues.length) {
|
|
deductWidth += 20;
|
|
}
|
|
|
|
if (deductWidth !== 0) {
|
|
styles.width = `calc(100% - ${deductWidth}px)`;
|
|
}
|
|
|
|
return styles;
|
|
},
|
|
hasRemoteMethod(): boolean {
|
|
return !!this.getArgument('loadOptionsMethod') || !!this.getArgument('loadOptions');
|
|
},
|
|
shortPath(): string {
|
|
const shortPath = this.path.split('.');
|
|
shortPath.shift();
|
|
return shortPath.join('.');
|
|
},
|
|
workflow(): Workflow {
|
|
return this.getCurrentWorkflow();
|
|
},
|
|
isResourceLocatorParameter(): boolean {
|
|
return this.parameter.type === 'resourceLocator';
|
|
},
|
|
isSecretParameter(): boolean {
|
|
return this.getArgument('password') === true;
|
|
},
|
|
remoteParameterOptionsKeys(): string[] {
|
|
return (this.remoteParameterOptions || []).map((o) => o.name);
|
|
},
|
|
shouldRedactValue(): boolean {
|
|
return this.getStringInputType === 'password' || this.isForCredential;
|
|
},
|
|
},
|
|
methods: {
|
|
isRemoteParameterOption(option: INodePropertyOptions) {
|
|
return this.remoteParameterOptionsKeys.includes(option.name);
|
|
},
|
|
credentialSelected(updateInformation: INodeUpdatePropertiesInformation) {
|
|
// Update the values on the node
|
|
this.workflowsStore.updateNodeProperties(updateInformation);
|
|
|
|
const node = this.workflowsStore.getNodeByName(updateInformation.name);
|
|
|
|
if (node) {
|
|
// Update the issues
|
|
this.updateNodeCredentialIssues(node);
|
|
}
|
|
|
|
void 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.i18n.credText().placeholder(this.parameter)
|
|
: this.i18n.nodeText().placeholder(this.parameter, this.path);
|
|
},
|
|
getOptionsOptionDisplayName(option: INodePropertyOptions): string {
|
|
return this.isForCredential
|
|
? this.i18n.credText().optionsOptionDisplayName(this.parameter, option)
|
|
: this.i18n.nodeText().optionsOptionDisplayName(this.parameter, option, this.path);
|
|
},
|
|
getOptionsOptionDescription(option: INodePropertyOptions): string {
|
|
return this.isForCredential
|
|
? this.i18n.credText().optionsOptionDescription(this.parameter, option)
|
|
: this.i18n.nodeText().optionsOptionDescription(this.parameter, option, this.path);
|
|
},
|
|
|
|
async loadRemoteParameterOptions() {
|
|
if (
|
|
this.node === null ||
|
|
!this.hasRemoteMethod ||
|
|
this.remoteParameterOptionsLoading ||
|
|
!this.parameter
|
|
) {
|
|
return;
|
|
}
|
|
this.remoteParameterOptionsLoadingIssues = null;
|
|
this.remoteParameterOptionsLoading = true;
|
|
this.remoteParameterOptions.length = 0;
|
|
|
|
// Get the resolved parameter values of the current node
|
|
|
|
try {
|
|
const currentNodeParameters = (this.ndvStore.activeNode as INodeUi).parameters;
|
|
const resolvedNodeParameters = this.resolveRequiredParameters(
|
|
this.parameter,
|
|
currentNodeParameters,
|
|
) as INodeParameters;
|
|
const loadOptionsMethod = this.getArgument('loadOptionsMethod') as string | undefined;
|
|
const loadOptions = this.getArgument('loadOptions') as ILoadOptions | undefined;
|
|
|
|
const options = await this.nodeTypesStore.getNodeParameterOptions({
|
|
nodeTypeAndVersion: {
|
|
name: this.node.type,
|
|
version: this.node.typeVersion,
|
|
},
|
|
path: this.path,
|
|
methodName: loadOptionsMethod,
|
|
loadOptions,
|
|
currentNodeParameters: resolvedNodeParameters,
|
|
credentials: this.node.credentials,
|
|
});
|
|
|
|
this.remoteParameterOptions.push.apply(this.remoteParameterOptions, options);
|
|
} catch (error) {
|
|
this.remoteParameterOptionsLoadingIssues = error.message;
|
|
}
|
|
|
|
this.remoteParameterOptionsLoading = false;
|
|
},
|
|
closeCodeEditDialog() {
|
|
this.codeEditDialogVisible = false;
|
|
|
|
this.editDialogClosing = true;
|
|
void this.$nextTick(() => {
|
|
this.editDialogClosing = false;
|
|
});
|
|
},
|
|
closeExpressionEditDialog() {
|
|
this.expressionEditDialogVisible = false;
|
|
},
|
|
trackExpressionEditOpen() {
|
|
if (!this.node) {
|
|
return;
|
|
}
|
|
|
|
if (this.node.type.startsWith('n8n-nodes-base') || isCredentialOnlyNodeType(this.node.type)) {
|
|
this.$telemetry.track('User opened Expression Editor', {
|
|
node_type: this.node.type,
|
|
parameter_name: this.parameter.displayName,
|
|
parameter_field_type: this.parameter.type,
|
|
new_expression: !this.isValueExpression,
|
|
workflow_id: this.workflowsStore.workflowId,
|
|
session_id: this.ndvStore.sessionId,
|
|
source: this.eventSource || 'ndv',
|
|
});
|
|
}
|
|
},
|
|
closeTextEditDialog() {
|
|
this.textEditDialogVisible = false;
|
|
|
|
this.editDialogClosing = true;
|
|
void this.$nextTick(() => {
|
|
this.$refs.inputField?.blur?.();
|
|
this.editDialogClosing = false;
|
|
});
|
|
},
|
|
displayEditDialog() {
|
|
if (this.editDialogClosing) {
|
|
return;
|
|
}
|
|
|
|
if (this.editorType) {
|
|
this.codeEditDialogVisible = true;
|
|
} else {
|
|
this.textEditDialogVisible = true;
|
|
}
|
|
},
|
|
getArgument(argumentName: string): string | number | boolean | undefined {
|
|
return this.parameter.typeOptions?.[argumentName];
|
|
},
|
|
expressionUpdated(value: string) {
|
|
const val: NodeParameterValueType = this.isResourceLocatorParameter
|
|
? { __rl: true, value, mode: this.modelValue.mode }
|
|
: value;
|
|
this.valueChanged(val);
|
|
},
|
|
openExpressionEditorModal() {
|
|
if (!this.isValueExpression) return;
|
|
|
|
this.expressionEditDialogVisible = true;
|
|
this.trackExpressionEditOpen();
|
|
},
|
|
onBlur() {
|
|
this.$emit('blur');
|
|
this.isFocused = false;
|
|
},
|
|
onResourceLocatorDrop(data: string) {
|
|
this.$emit('drop', data);
|
|
},
|
|
async setFocus(event: MouseEvent) {
|
|
if (['json'].includes(this.parameter.type) && this.getArgument('alwaysOpenEditWindow')) {
|
|
this.displayEditDialog();
|
|
return;
|
|
}
|
|
|
|
if (this.node !== null) {
|
|
// When an event like mouse-click removes the active node while
|
|
// editing is active it does not know where to save the value to.
|
|
// For that reason do we save the node-name here. We could probably
|
|
// also just do that once on load but if Vue decides for some reason to
|
|
// reuse the input it could have the wrong value so lets set it everytime
|
|
// just to be sure
|
|
this.nodeName = this.node.name;
|
|
}
|
|
|
|
await this.$nextTick();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents
|
|
const inputRef = this.$refs.inputField as InstanceType<N8nInput> | undefined;
|
|
if (inputRef?.$el) {
|
|
if (inputRef.focusOnInput) {
|
|
inputRef.focusOnInput();
|
|
} else if (inputRef.focus) {
|
|
inputRef.focus();
|
|
}
|
|
|
|
this.isFocused = true;
|
|
}
|
|
|
|
this.$emit('focus');
|
|
},
|
|
isCodeNode(node: INodeUi): boolean {
|
|
return NODES_USING_CODE_NODE_EDITOR.includes(node.type);
|
|
},
|
|
isHtmlNode(node: INodeUi): boolean {
|
|
return node.type === HTML_NODE_TYPE;
|
|
},
|
|
isExecuteWorkflowNode(node: INodeUi): boolean {
|
|
return node.type === EXECUTE_WORKFLOW_NODE_TYPE;
|
|
},
|
|
rgbaToHex(value: string): string | null {
|
|
// Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
|
|
const valueMatch = value.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/);
|
|
if (valueMatch === null) {
|
|
// TODO: Display something if value is not valid
|
|
return null;
|
|
}
|
|
const [r, g, b, a] = valueMatch.splice(1, 4).map((v) => Number(v));
|
|
return (
|
|
'#' +
|
|
((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1) +
|
|
((1 << 8) + Math.floor((1 - a) * 255)).toString(16).slice(1)
|
|
);
|
|
},
|
|
onTextInputChange(value: string) {
|
|
const parameterData = {
|
|
node: this.node !== null ? this.node.name : this.nodeName,
|
|
name: this.path,
|
|
value,
|
|
};
|
|
|
|
this.$emit('textInput', parameterData);
|
|
},
|
|
valueChangedDebounced(value: NodeParameterValueType | {} | Date) {
|
|
void this.callDebounced('valueChanged', { debounceTime: 100 }, value);
|
|
},
|
|
onUpdateTextInput(value: string) {
|
|
this.valueChanged(value);
|
|
this.onTextInputChange(value);
|
|
},
|
|
valueChanged(value: NodeParameterValueType | {} | Date) {
|
|
if (this.parameter.name === 'nodeCredentialType') {
|
|
this.activeCredentialType = value as string;
|
|
}
|
|
|
|
if (value instanceof Date) {
|
|
value = value.toISOString();
|
|
}
|
|
|
|
if (
|
|
this.parameter.type === 'color' &&
|
|
this.getArgument('showAlpha') === true &&
|
|
value !== null &&
|
|
value !== undefined &&
|
|
value.toString().charAt(0) !== '#'
|
|
) {
|
|
const newValue = this.rgbaToHex(value as string);
|
|
if (newValue !== null) {
|
|
this.tempValue = newValue;
|
|
value = newValue;
|
|
}
|
|
}
|
|
|
|
const parameterData = {
|
|
node: this.node !== null ? this.node.name : this.nodeName,
|
|
name: this.path,
|
|
value,
|
|
};
|
|
|
|
this.$emit('update', parameterData);
|
|
|
|
if (this.parameter.name === 'operation' || this.parameter.name === 'mode') {
|
|
this.$telemetry.track('User set node operation or mode', {
|
|
workflow_id: this.workflowsStore.workflowId,
|
|
node_type: this.node && this.node.type,
|
|
resource: this.node && this.node.parameters.resource,
|
|
is_custom: value === CUSTOM_API_CALL_KEY,
|
|
session_id: this.ndvStore.sessionId,
|
|
parameter: this.parameter.name,
|
|
});
|
|
}
|
|
},
|
|
async optionSelected(command: string) {
|
|
const prevValue = this.modelValue;
|
|
|
|
if (command === 'resetValue') {
|
|
this.valueChanged(this.parameter.default);
|
|
} else if (command === 'addExpression') {
|
|
if (this.isResourceLocatorParameter) {
|
|
if (isResourceLocatorValue(this.modelValue)) {
|
|
this.valueChanged({
|
|
__rl: true,
|
|
value: `=${this.modelValue.value}`,
|
|
mode: this.modelValue.mode,
|
|
});
|
|
} else {
|
|
this.valueChanged({ __rl: true, value: `=${this.modelValue}`, mode: '' });
|
|
}
|
|
} else if (
|
|
this.parameter.type === 'number' &&
|
|
(!this.modelValue || this.modelValue === '[Object: null]')
|
|
) {
|
|
this.valueChanged('={{ 0 }}');
|
|
} else if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
|
|
this.valueChanged(`={{ ${this.modelValue} }}`);
|
|
} else {
|
|
this.valueChanged(`=${this.modelValue}`);
|
|
}
|
|
|
|
await this.setFocus();
|
|
} else if (command === 'removeExpression') {
|
|
let value: NodeParameterValueType = this.expressionEvaluated;
|
|
|
|
this.isFocused = false;
|
|
|
|
if (this.parameter.type === 'multiOptions' && typeof value === 'string') {
|
|
value = (value || '')
|
|
.split(',')
|
|
.filter((value) =>
|
|
(this.parameterOptions || []).find(
|
|
(option) => (option as INodePropertyOptions).value === value,
|
|
),
|
|
);
|
|
}
|
|
|
|
if (this.isResourceLocatorParameter && isResourceLocatorValue(this.modelValue)) {
|
|
this.valueChanged({ __rl: true, value, mode: this.modelValue.mode });
|
|
} else {
|
|
let newValue = typeof value !== 'undefined' ? value : null;
|
|
|
|
if (this.parameter.type === 'string') {
|
|
// Strip the '=' from the beginning
|
|
newValue = this.modelValue ? this.modelValue.toString().substring(1) : null;
|
|
}
|
|
|
|
this.valueChanged(newValue);
|
|
}
|
|
} else if (command === 'refreshOptions') {
|
|
if (this.isResourceLocatorParameter) {
|
|
this.eventBus.emit('refreshList');
|
|
}
|
|
void this.loadRemoteParameterOptions();
|
|
} else if (command === 'formatHtml') {
|
|
htmlEditorEventBus.emit('format-html');
|
|
}
|
|
|
|
if (this.node && (command === 'addExpression' || command === 'removeExpression')) {
|
|
const telemetryPayload = {
|
|
node_type: this.node.type,
|
|
parameter: this.path,
|
|
old_mode: command === 'addExpression' ? 'fixed' : 'expression',
|
|
new_mode: command === 'removeExpression' ? 'fixed' : 'expression',
|
|
was_parameter_empty: prevValue === '' || prevValue === undefined,
|
|
had_mapping: hasExpressionMapping(prevValue),
|
|
had_parameter: typeof prevValue === 'string' && prevValue.includes('$parameter'),
|
|
};
|
|
this.$telemetry.track('User switched parameter mode', telemetryPayload);
|
|
void this.externalHooks.run('parameterInput.modeSwitch', telemetryPayload);
|
|
}
|
|
},
|
|
},
|
|
async updated() {
|
|
await this.$nextTick();
|
|
const remoteParameterOptions = this.$el.querySelectorAll('.remote-parameter-option');
|
|
|
|
if (remoteParameterOptions.length > 0) {
|
|
void this.externalHooks.run('parameterInput.updated', { remoteParameterOptions });
|
|
}
|
|
},
|
|
mounted() {
|
|
this.eventBus.on('optionSelected', this.optionSelected);
|
|
|
|
this.tempValue = this.displayValue as string;
|
|
if (this.node !== null) {
|
|
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) {
|
|
this.tempValue = newValue;
|
|
}
|
|
}
|
|
|
|
if (this.hasRemoteMethod && this.node !== null) {
|
|
// Make sure to load the parameter options
|
|
// directly and whenever the credentials change
|
|
this.$watch(
|
|
() => this.node?.credentials,
|
|
() => {
|
|
void this.loadRemoteParameterOptions();
|
|
},
|
|
{ immediate: true },
|
|
);
|
|
}
|
|
|
|
void this.externalHooks.run('parameterInput.mount', {
|
|
parameter: this.parameter,
|
|
inputFieldRef: this.$refs.inputField as InstanceType<typeof N8nInput>,
|
|
});
|
|
},
|
|
beforeUnmount() {
|
|
this.eventBus.off('optionSelected', this.optionSelected);
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.readonly-code {
|
|
font-size: var(--font-size-xs);
|
|
}
|
|
|
|
.switch-input {
|
|
margin: var(--spacing-5xs) 0 var(--spacing-2xs) 0;
|
|
}
|
|
|
|
.parameter-value-container {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.parameter-actions {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.parameter-input {
|
|
display: inline-block;
|
|
|
|
:deep(.color-input) {
|
|
display: flex;
|
|
|
|
.el-color-picker__trigger {
|
|
border: none;
|
|
}
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<style lang="scss">
|
|
.ql-editor {
|
|
padding: 6px;
|
|
line-height: 26px;
|
|
background-color: #f0f0f0;
|
|
}
|
|
|
|
.droppable {
|
|
--input-border-color: var(--color-ndv-droppable-parameter);
|
|
--input-border-style: dashed;
|
|
|
|
textarea,
|
|
input,
|
|
.cm-editor {
|
|
border-width: 1.5px;
|
|
}
|
|
}
|
|
|
|
.activeDrop {
|
|
--input-border-color: var(--color-success);
|
|
--input-background-color: var(--color-foreground-xlight);
|
|
--input-border-style: solid;
|
|
|
|
textarea,
|
|
input {
|
|
cursor: grabbing !important;
|
|
border-width: 1px;
|
|
}
|
|
}
|
|
|
|
.has-issues {
|
|
--input-border-color: var(--color-danger);
|
|
}
|
|
|
|
.el-dropdown {
|
|
color: var(--color-text-light);
|
|
}
|
|
|
|
.list-option {
|
|
margin: 6px 0;
|
|
white-space: normal;
|
|
padding-right: 20px;
|
|
|
|
.option-headline {
|
|
font-weight: var(--font-weight-bold);
|
|
line-height: var(--font-line-height-regular);
|
|
overflow-wrap: break-word;
|
|
}
|
|
|
|
.option-description {
|
|
margin-top: 2px;
|
|
font-size: var(--font-size-2xs);
|
|
font-weight: var(--font-weight-regular);
|
|
line-height: var(--font-line-height-xloose);
|
|
color: $custom-font-very-light;
|
|
}
|
|
}
|
|
|
|
.edit-window-button {
|
|
display: none;
|
|
}
|
|
|
|
.parameter-input:hover .edit-window-button {
|
|
display: inline;
|
|
}
|
|
|
|
.expand-input-icon-container {
|
|
display: flex;
|
|
height: 100%;
|
|
align-items: center;
|
|
}
|
|
|
|
.input-with-opener .el-input__suffix {
|
|
right: 0;
|
|
}
|
|
|
|
.el-input--suffix .el-input__inner {
|
|
padding-right: 0;
|
|
}
|
|
|
|
.textarea-modal-opener {
|
|
position: absolute;
|
|
right: 0;
|
|
bottom: 0;
|
|
background-color: var(--color-code-background);
|
|
padding: 3px;
|
|
line-height: 9px;
|
|
border: var(--border-base);
|
|
border-top-left-radius: var(--border-radius-base);
|
|
border-bottom-right-radius: var(--border-radius-base);
|
|
cursor: pointer;
|
|
|
|
svg {
|
|
width: 9px !important;
|
|
height: 9px;
|
|
transform: rotate(270deg);
|
|
|
|
&:hover {
|
|
color: var(--color-primary);
|
|
}
|
|
}
|
|
}
|
|
|
|
.focused {
|
|
border-color: var(--color-secondary);
|
|
}
|
|
|
|
.invalid {
|
|
border-color: var(--color-danger);
|
|
}
|
|
</style>
|