refactor: Fix type issues for parameter input components (#9449)

This commit is contained in:
Elias Meire 2024-05-21 15:04:20 +02:00 committed by GitHub
parent cd751e7cc8
commit 711c46f205
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 315 additions and 243 deletions

View file

@ -37,4 +37,4 @@ export namespace n8n {
} }
} }
export type ExtendedValidationResult = Partial<ValidationResult> & { fieldName?: string }; export type ExtendedValidationResult = ValidationResult & { fieldName?: string };

View file

@ -99,6 +99,7 @@ import type {
WorkflowActivateMode, WorkflowActivateMode,
WorkflowExecuteMode, WorkflowExecuteMode,
CallbackManager, CallbackManager,
INodeParameters,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
ExpressionError, ExpressionError,
@ -2121,13 +2122,12 @@ export function cleanupParameterData(inputData: NodeParameterValueType): void {
} }
if (typeof inputData === 'object') { if (typeof inputData === 'object') {
type Key = keyof typeof inputData; Object.keys(inputData).forEach((key) => {
(Object.keys(inputData) as Key[]).forEach((key) => { const value = (inputData as INodeParameters)[key];
const value = inputData[key];
if (typeof value === 'object') { if (typeof value === 'object') {
if (DateTime.isDateTime(value)) { if (DateTime.isDateTime(value)) {
// Is a special luxon date so convert to string // Is a special luxon date so convert to string
inputData[key] = value.toString(); (inputData as INodeParameters)[key] = value.toString();
} else { } else {
cleanupParameterData(value); cleanupParameterData(value);
} }
@ -2230,6 +2230,7 @@ const validateCollection = (
return validationResult; return validationResult;
} }
if (validationResult.valid) {
for (const value of Array.isArray(validationResult.newValue) for (const value of Array.isArray(validationResult.newValue)
? (validationResult.newValue as IDataObject[]) ? (validationResult.newValue as IDataObject[])
: [validationResult.newValue as IDataObject]) { : [validationResult.newValue as IDataObject]) {
@ -2254,6 +2255,7 @@ const validateCollection = (
value[key] = fieldValidationResult.newValue; value[key] = fieldValidationResult.newValue;
} }
} }
}
return validationResult; return validationResult;
}; };

View file

@ -130,15 +130,17 @@ export type EndpointStyle = {
hoverMessage?: string; hoverMessage?: string;
}; };
export interface IUpdateInformation { export interface IUpdateInformation<
name: string; T extends NodeParameterValueType =
key?: string;
value:
| string | string
| number | number
| { [key: string]: string | number | boolean } | { [key: string]: string | number | boolean }
| NodeParameterValueType | NodeParameterValueType
| INodeParameters; // with null makes problems in NodeSettings.vue | INodeParameters,
> {
name: string;
key?: string;
value: T;
node?: string; node?: string;
oldValue?: string | number; oldValue?: string | number;
type?: 'optionsOrderChanged'; type?: 'optionsOrderChanged';

View file

@ -1,20 +1,27 @@
import { defineComponent } from 'vue';
import type { Diagnostic } from '@codemirror/lint'; import type { Diagnostic } from '@codemirror/lint';
import { linter as createLinter } from '@codemirror/lint'; import { linter as createLinter } from '@codemirror/lint';
import type { EditorView } from '@codemirror/view'; import type { EditorView } from '@codemirror/view';
import * as esprima from 'esprima-next'; import * as esprima from 'esprima-next';
import type { Node } from 'estree'; import type { Node } from 'estree';
import type { CodeNodeEditorLanguage } from 'n8n-workflow'; import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow';
import { type PropType, defineComponent } from 'vue';
import { import {
DEFAULT_LINTER_DELAY_IN_MS, DEFAULT_LINTER_DELAY_IN_MS,
DEFAULT_LINTER_SEVERITY, DEFAULT_LINTER_SEVERITY,
OFFSET_FOR_SCRIPT_WRAPPER, OFFSET_FOR_SCRIPT_WRAPPER,
} from './constants'; } from './constants';
import { walk } from './utils';
import type { RangeNode } from './types'; import type { RangeNode } from './types';
import { walk } from './utils';
export const linterExtension = defineComponent({ export const linterExtension = defineComponent({
props: {
mode: {
type: String as PropType<CodeExecutionMode>,
required: true,
},
editor: { type: Object as PropType<EditorView | null>, default: null },
},
methods: { methods: {
createLinter(language: CodeNodeEditorLanguage) { createLinter(language: CodeNodeEditorLanguage) {
switch (language) { switch (language) {

View file

@ -72,7 +72,7 @@ export interface Props {
nodeValues: INodeParameters; nodeValues: INodeParameters;
parameter: INodeProperties; parameter: INodeProperties;
path: string; path: string;
values: INodeProperties; values: INodeParameters;
isReadOnly?: boolean; isReadOnly?: boolean;
} }
const emit = defineEmits<{ const emit = defineEmits<{

View file

@ -25,14 +25,18 @@ export default defineComponent({
name: 'ExpandableInputEdit', name: 'ExpandableInputEdit',
components: { ExpandableInputBase }, components: { ExpandableInputBase },
props: { props: {
modelValue: {}, modelValue: {
placeholder: {}, type: String,
maxlength: {}, required: true,
autofocus: {}, },
placeholder: { type: String, required: true },
maxlength: { type: Number },
autofocus: { type: Boolean },
eventBus: { eventBus: {
type: Object as PropType<EventBus>, type: Object as PropType<EventBus>,
}, },
}, },
emits: ['update:modelValue', 'enter', 'blur', 'esc'],
mounted() { mounted() {
// autofocus on input element is not reliable // autofocus on input element is not reliable
if (this.autofocus && this.$refs.input) { if (this.autofocus && this.$refs.input) {

View file

@ -80,7 +80,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { type PropType, defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import ExpressionEditorModalInput from '@/components/ExpressionEditorModal/ExpressionEditorModalInput.vue'; import ExpressionEditorModalInput from '@/components/ExpressionEditorModal/ExpressionEditorModalInput.vue';
import VariableSelector from '@/components/VariableSelector.vue'; import VariableSelector from '@/components/VariableSelector.vue';
@ -98,6 +98,7 @@ import { useDebounce } from '@/composables/useDebounce';
import type { Segment } from '@/types/expressions'; import type { Segment } from '@/types/expressions';
import ExpressionOutput from './InlineExpressionEditor/ExpressionOutput.vue'; import ExpressionOutput from './InlineExpressionEditor/ExpressionOutput.vue';
import { outputTheme } from './ExpressionEditorModal/theme'; import { outputTheme } from './ExpressionEditorModal/theme';
import type { INodeProperties } from 'n8n-workflow';
export default defineComponent({ export default defineComponent({
name: 'ExpressionEdit', name: 'ExpressionEdit',
@ -112,7 +113,7 @@ export default defineComponent({
default: false, default: false,
}, },
parameter: { parameter: {
type: Object, type: Object as PropType<INodeProperties>,
default: () => ({}), default: () => ({}),
}, },
path: { path: {

View file

@ -23,16 +23,17 @@ const inlineInput = ref<InstanceType<typeof InlineExpressionEditorInput>>();
type Props = { type Props = {
path: string; path: string;
modelValue: string; modelValue: string;
isReadOnly: boolean; rows?: number;
rows: number; additionalExpressionData?: IDataObject;
isAssignment: boolean; eventBus?: EventBus;
additionalExpressionData: IDataObject; isReadOnly?: boolean;
eventBus: EventBus; isAssignment?: boolean;
}; };
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
rows: 5, rows: 5,
isAssignment: false, isAssignment: false,
isReadOnly: false,
additionalExpressionData: () => ({}), additionalExpressionData: () => ({}),
eventBus: () => createEventBus(), eventBus: () => createEventBus(),
}); });

View file

@ -5,10 +5,11 @@ import ParameterInputFull from '@/components/ParameterInputFull.vue';
import ParameterIssues from '@/components/ParameterIssues.vue'; import ParameterIssues from '@/components/ParameterIssues.vue';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { import type {
type FilterConditionValue, FilterConditionValue,
type FilterOptionsValue, FilterOptionsValue,
type INodeProperties, INodeProperties,
NodeParameterValue,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import OperatorSelect from './OperatorSelect.vue'; import OperatorSelect from './OperatorSelect.vue';
@ -100,11 +101,11 @@ const rightParameter = computed<INodeProperties>(() => {
}; };
}); });
const onLeftValueChange = (update: IUpdateInformation): void => { const onLeftValueChange = (update: IUpdateInformation<NodeParameterValue>): void => {
condition.value.leftValue = update.value; condition.value.leftValue = update.value;
}; };
const onRightValueChange = (update: IUpdateInformation): void => { const onRightValueChange = (update: IUpdateInformation<NodeParameterValue>): void => {
condition.value.rightValue = update.value; condition.value.rightValue = update.value;
}; };

View file

@ -17,12 +17,24 @@ import type { ConditionResult, FilterOperator } from './types';
export const getFilterOperator = (key: string) => export const getFilterOperator = (key: string) =>
OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator; OPERATORS_BY_ID[key as FilterOperatorId] as FilterOperator;
const convertToType = (value: unknown, type: FilterOperatorType): unknown => { const getTargetType = (type: FilterOperatorType) => {
if (type === 'number') return 'number';
if (type === 'boolean') return 'boolean';
return 'string';
};
const convertToType = (value: NodeParameterValue, type: FilterOperatorType): NodeParameterValue => {
if (type === 'any') return value; if (type === 'any') return value;
const fallback = type === 'boolean' ? false : value; const fallback = type === 'boolean' ? false : value;
return validateFieldType('filter', value, type, { parseStrings: true }).newValue ?? fallback; const validationResult = validateFieldType('filter', value, getTargetType(type), {
parseStrings: true,
});
if (!validationResult.valid) {
return fallback;
}
return validationResult.newValue ?? fallback;
}; };
export const handleOperatorChange = ({ export const handleOperatorChange = ({
@ -42,6 +54,7 @@ export const handleOperatorChange = ({
if (leftTypeChanged && !isExpression(condition.leftValue)) { if (leftTypeChanged && !isExpression(condition.leftValue)) {
condition.leftValue = convertToType(condition.leftValue, newOperator.type); condition.leftValue = convertToType(condition.leftValue, newOperator.type);
} }
if (rightTypeChanged && !newOperator.singleValue && !isExpression(condition.rightValue)) { if (rightTypeChanged && !newOperator.singleValue && !isExpression(condition.rightValue)) {
condition.rightValue = convertToType(condition.rightValue, newRightType); condition.rightValue = convertToType(condition.rightValue, newRightType);
} }

View file

@ -95,7 +95,7 @@
<div v-if="parameterOptions.length > 0 && !isReadOnly" class="controls"> <div v-if="parameterOptions.length > 0 && !isReadOnly" class="controls">
<n8n-button <n8n-button
v-if="parameter.options.length === 1" v-if="parameter.options && parameter.options.length === 1"
type="tertiary" type="tertiary"
block block
:label="getPlaceholderText" :label="getPlaceholderText"
@ -126,12 +126,7 @@ import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { IUpdateInformation } from '@/Interface'; import type { IUpdateInformation } from '@/Interface';
import type { import type { INodeParameters, INodeProperties, INodePropertyCollection } from 'n8n-workflow';
INodeParameters,
INodeProperties,
INodePropertyCollection,
NodeParameterValue,
} from 'n8n-workflow';
import { deepCopy, isINodePropertyCollectionList } from 'n8n-workflow'; import { deepCopy, isINodePropertyCollectionList } from 'n8n-workflow';
import { get } from 'lodash-es'; import { get } from 'lodash-es';
@ -140,7 +135,7 @@ export default defineComponent({
name: 'FixedCollectionParameter', name: 'FixedCollectionParameter',
props: { props: {
nodeValues: { nodeValues: {
type: Object as PropType<Record<string, INodeParameters[]>>, type: Object as PropType<INodeParameters>,
required: true, required: true,
}, },
parameter: { parameter: {
@ -297,23 +292,18 @@ export default defineComponent({
optionParameter.typeOptions.multipleValues === true optionParameter.typeOptions.multipleValues === true
) { ) {
// Multiple values are allowed so append option to array // Multiple values are allowed so append option to array
newParameterValue[optionParameter.name] = get( const multiValue = get(this.nodeValues, [this.path, optionParameter.name], []);
this.nodeValues,
[this.path, optionParameter.name],
[],
);
if (Array.isArray(optionParameter.default)) { if (Array.isArray(optionParameter.default)) {
(newParameterValue[optionParameter.name] as INodeParameters[]).push( multiValue.push(...deepCopy(optionParameter.default));
...deepCopy(optionParameter.default as INodeParameters[]),
);
} else if ( } else if (
optionParameter.default !== '' && optionParameter.default !== '' &&
typeof optionParameter.default !== 'object' typeof optionParameter.default !== 'object'
) { ) {
(newParameterValue[optionParameter.name] as NodeParameterValue[]).push( multiValue.push(deepCopy(optionParameter.default));
deepCopy(optionParameter.default),
);
} }
newParameterValue[optionParameter.name] = multiValue;
} else { } else {
// Add a new option // Add a new option
newParameterValue[optionParameter.name] = deepCopy(optionParameter.default); newParameterValue[optionParameter.name] = deepCopy(optionParameter.default);
@ -322,7 +312,7 @@ export default defineComponent({
let newValue; let newValue;
if (this.multipleValues) { if (this.multipleValues) {
newValue = get(this.nodeValues, name, [] as INodeParameters[]); newValue = get(this.nodeValues, name, []) as INodeParameters[];
newValue.push(newParameterValue); newValue.push(newParameterValue);
} else { } else {

View file

@ -102,7 +102,7 @@ export default defineComponent({
}, },
props: { props: {
nodeValues: { nodeValues: {
type: Object as PropType<Record<string, INodeParameters[]>>, type: Object as PropType<INodeParameters>,
required: true, required: true,
}, },
parameter: { parameter: {
@ -159,7 +159,7 @@ export default defineComponent({
methods: { methods: {
addItem() { addItem() {
const name = this.getPath(); const name = this.getPath();
const currentValue = get(this.nodeValues, name, [] as INodeParameters[]); const currentValue = get(this.nodeValues, name, []) as INodeParameters[];
currentValue.push(deepCopy(this.parameter.default as INodeParameters)); currentValue.push(deepCopy(this.parameter.default as INodeParameters));

View file

@ -33,7 +33,12 @@ export default defineComponent({
props: { props: {
nodeType: { nodeType: {
type: Object as PropType< type: Object as PropType<
INodeTypeDescription | IVersionNode | SimplifiedNodeType | ActionTypeDescription | null | INodeTypeDescription
| IVersionNode
| SimplifiedNodeType
| ActionTypeDescription
| null
| undefined
>, >,
required: true, required: true,
}, },

View file

@ -1,6 +1,8 @@
<template> <template>
<span :class="$style.container" data-test-id="node-title-container" @click="onEdit"> <span :class="$style.container" data-test-id="node-title-container" @click="onEdit">
<span :class="$style.iconWrapper"><NodeIcon :node-type="nodeType" :size="18" /></span> <span :class="$style.iconWrapper">
<NodeIcon :node-type="nodeType" :size="18" />
</span>
<n8n-popover placement="right" width="200" :visible="editName" :disabled="!editable"> <n8n-popover placement="right" width="200" :visible="editName" :disabled="!editable">
<div <div
:class="$style.editContainer" :class="$style.editContainer"
@ -39,56 +41,47 @@
</span> </span>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import NodeIcon from '@/components/NodeIcon.vue'; import NodeIcon from '@/components/NodeIcon.vue';
import { defineComponent } from 'vue'; import type { INodeTypeDescription } from 'n8n-workflow';
import { computed, nextTick, ref } from 'vue';
export default defineComponent({ type Props = {
name: 'NodeTitle', modelValue: string;
components: { nodeType?: INodeTypeDescription;
NodeIcon, readOnly?: boolean;
},
props: {
modelValue: {
type: String,
default: '',
},
nodeType: {},
readOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
editName: false,
newName: '',
}; };
},
computed: { const props = withDefaults(defineProps<Props>(), {
editable(): boolean { modelValue: '',
return !this.readOnly && window === window.parent; nodeType: undefined,
}, readOnly: false,
}, });
methods: { const emit = defineEmits<{
async onEdit() { (event: 'update:model-value', value: string): void;
this.newName = this.modelValue; }>();
this.editName = true; const editName = ref(false);
await this.$nextTick(); const newName = ref('');
const inputRef = this.$refs.input as HTMLInputElement | undefined; const input = ref<HTMLInputElement>();
if (inputRef) {
inputRef.focus(); const editable = computed(() => !props.readOnly && window === window.parent);
async function onEdit() {
newName.value = props.modelValue;
editName.value = true;
await nextTick();
if (input.value) {
input.value.focus();
} }
},
onRename() {
if (this.newName.trim() !== '') {
this.$emit('update:modelValue', this.newName.trim());
} }
this.editName = false; function onRename() {
}, if (newName.value.trim() !== '') {
}, emit('update:model-value', newName.value.trim());
}); }
editName.value = false;
}
</script> </script>
<style lang="scss" module> <style lang="scss" module>

View file

@ -63,7 +63,7 @@ export default defineComponent({
props: ['modalName'], props: ['modalName'],
setup() { setup() {
return { return {
...useToast(), toast: useToast(),
}; };
}, },
data() { data() {
@ -94,7 +94,7 @@ export default defineComponent({
try { try {
await this.uiStore.applyForOnboardingCall(this.email); await this.uiStore.applyForOnboardingCall(this.email);
this.showMessage({ this.toast.showMessage({
type: 'success', type: 'success',
title: this.$locale.baseText('onboardingCallSignupSucess.title'), title: this.$locale.baseText('onboardingCallSignupSucess.title'),
message: this.$locale.baseText('onboardingCallSignupSucess.message'), message: this.$locale.baseText('onboardingCallSignupSucess.message'),
@ -102,7 +102,7 @@ export default defineComponent({
this.okToClose = true; this.okToClose = true;
this.modalBus.emit('close'); this.modalBus.emit('close');
} catch (e) { } catch (e) {
this.showError( this.toast.showError(
e, e,
this.$locale.baseText('onboardingCallSignupFailed.title'), this.$locale.baseText('onboardingCallSignupFailed.title'),
this.$locale.baseText('onboardingCallSignupFailed.message'), this.$locale.baseText('onboardingCallSignupFailed.message'),

View file

@ -80,7 +80,7 @@
</n8n-text> </n8n-text>
</template> </template>
<template v-if="outputMode === 'logs'" #content> <template v-if="outputMode === 'logs' && node" #content>
<RunDataAi :node="node" :run-index="runIndex" /> <RunDataAi :node="node" :run-index="runIndex" />
</template> </template>
<template #recovered-artificial-output-data> <template #recovered-artificial-output-data>
@ -123,6 +123,9 @@ const OUTPUT_TYPE = {
LOGS: 'logs', LOGS: 'logs',
} as const; } as const;
type OutputTypeKey = keyof typeof OUTPUT_TYPE;
type OutputType = (typeof OUTPUT_TYPE)[OutputTypeKey];
export default defineComponent({ export default defineComponent({
name: 'OutputPanel', name: 'OutputPanel',
components: { RunData, RunInfo, RunDataAi }, components: { RunData, RunInfo, RunDataAi },
@ -183,8 +186,8 @@ export default defineComponent({
}, },
computed: { computed: {
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
node(): INodeUi | null { node(): INodeUi | undefined {
return this.ndvStore.activeNode; return this.ndvStore.activeNode ?? undefined;
}, },
nodeType(): INodeTypeDescription | null { nodeType(): INodeTypeDescription | null {
if (this.node) { if (this.node) {
@ -193,7 +196,7 @@ export default defineComponent({
return null; return null;
}, },
isTriggerNode(): boolean { isTriggerNode(): boolean {
return this.nodeTypesStore.isTriggerNode(this.node?.type ?? ''); return !!this.node && this.nodeTypesStore.isTriggerNode(this.node.type);
}, },
hasAiMetadata(): boolean { hasAiMetadata(): boolean {
if (this.node) { if (this.node) {
@ -263,11 +266,11 @@ export default defineComponent({
const runData: IRunData | null = this.workflowRunData; const runData: IRunData | null = this.workflowRunData;
if (runData === null || !runData.hasOwnProperty(this.node.name)) { if (runData === null || (this.node && !runData.hasOwnProperty(this.node.name))) {
return 0; return 0;
} }
if (runData[this.node.name].length) { if (this.node && runData[this.node.name].length) {
return runData[this.node.name].length; return runData[this.node.name].length;
} }
@ -327,7 +330,7 @@ export default defineComponent({
onRunIndexChange(run: number) { onRunIndexChange(run: number) {
this.$emit('runChange', run); this.$emit('runChange', run);
}, },
onUpdateOutputMode(outputMode: (typeof OUTPUT_TYPE)[keyof typeof OUTPUT_TYPE]) { onUpdateOutputMode(outputMode: OutputType) {
if (outputMode === OUTPUT_TYPE.LOGS) { if (outputMode === OUTPUT_TYPE.LOGS) {
ndvEventBus.emit('setPositionByName', 'minLeft'); ndvEventBus.emit('setPositionByName', 'minLeft');
} else { } else {

View file

@ -4,7 +4,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { NotificationInstance } from 'element-plus'; import type { NotificationHandle } from 'element-plus';
import { sanitizeHtml } from '@/utils/htmlUtils'; import { sanitizeHtml } from '@/utils/htmlUtils';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
@ -21,16 +21,16 @@ export default defineComponent({
}, },
setup() { setup() {
return { return {
...useToast(), toast: useToast(),
}; };
}, },
data() { data() {
return { return {
alert: null as null | NotificationInstance, alert: null as NotificationHandle | null,
}; };
}, },
mounted() { mounted() {
this.alert = this.showAlert({ this.alert = this.toast.showAlert({
title: '', title: '',
message: sanitizeHtml(this.message), message: sanitizeHtml(this.message),
type: 'warning', type: 'warning',

View file

@ -537,7 +537,7 @@ type Props = {
hint?: string; hint?: string;
inputSize?: InputSize; inputSize?: InputSize;
eventSource?: string; eventSource?: string;
expressionEvaluated?: string; expressionEvaluated: unknown;
documentationUrl?: string; documentationUrl?: string;
isAssignment?: boolean; isAssignment?: boolean;
isReadOnly?: boolean; isReadOnly?: boolean;
@ -555,7 +555,6 @@ const props = withDefaults(defineProps<Props>(), {
hint: undefined, hint: undefined,
inputSize: undefined, inputSize: undefined,
eventSource: undefined, eventSource: undefined,
expressionEvaluated: undefined,
documentationUrl: undefined, documentationUrl: undefined,
isReadOnly: false, isReadOnly: false,
isAssignment: false, isAssignment: false,
@ -1266,7 +1265,7 @@ async function optionSelected(command: string) {
await setFocus(); await setFocus();
} else if (command === 'removeExpression') { } else if (command === 'removeExpression') {
let value: NodeParameterValueType = props.expressionEvaluated; let value = props.expressionEvaluated;
isFocused.value = false; isFocused.value = false;

View file

@ -30,7 +30,7 @@
:error-highlight="showRequiredErrors" :error-highlight="showRequiredErrors"
:is-for-credential="true" :is-for-credential="true"
:event-source="eventSource" :event-source="eventSource"
:hint="!showRequiredErrors ? hint : ''" :hint="!showRequiredErrors && hint ? hint : ''"
:event-bus="eventBus" :event-bus="eventBus"
@focus="onFocus" @focus="onFocus"
@blur="onBlur" @blur="onBlur"
@ -61,7 +61,12 @@ import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import ParameterInputWrapper from './ParameterInputWrapper.vue'; import ParameterInputWrapper from './ParameterInputWrapper.vue';
import { isValueExpression } from '@/utils/nodeTypesUtils'; import { isValueExpression } from '@/utils/nodeTypesUtils';
import type { INodeParameterResourceLocator, INodeProperties, IParameterLabel } from 'n8n-workflow'; import type {
INodeParameterResourceLocator,
INodeProperties,
IParameterLabel,
NodeParameterValueType,
} from 'n8n-workflow';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
@ -75,8 +80,11 @@ export default defineComponent({
props: { props: {
parameter: { parameter: {
type: Object as PropType<INodeProperties>, type: Object as PropType<INodeProperties>,
required: true,
},
value: {
type: Object as PropType<NodeParameterValueType>,
}, },
value: {},
showValidationWarnings: { showValidationWarnings: {
type: Boolean, type: Boolean,
}, },

View file

@ -150,6 +150,7 @@ export default defineComponent({
}, },
path: { path: {
type: String, type: String,
required: true,
}, },
value: { value: {
type: [Number, String, Boolean, Array, Object] as PropType<NodeParameterValueType>, type: [Number, String, Boolean, Array, Object] as PropType<NodeParameterValueType>,
@ -187,7 +188,7 @@ export default defineComponent({
node(): INodeUi | null { node(): INodeUi | null {
return this.ndvStore.activeNode; return this.ndvStore.activeNode;
}, },
hint(): string | null { hint(): string {
return this.i18n.nodeText().hint(this.parameter, this.path); return this.i18n.nodeText().hint(this.parameter, this.path);
}, },
isInputTypeString(): boolean { isInputTypeString(): boolean {
@ -260,8 +261,10 @@ export default defineComponent({
} }
}, },
onDrop(newParamValue: string) { onDrop(newParamValue: string) {
const updatedValue = getMappedResult(this.parameter, newParamValue, this.value); const value = this.value;
const prevValue = this.isResourceLocator ? this.value.value : this.value; const updatedValue = getMappedResult(this.parameter, newParamValue, value);
const prevValue =
this.isResourceLocator && isResourceLocatorValue(value) ? value.value : value;
if (updatedValue.startsWith('=')) { if (updatedValue.startsWith('=')) {
this.forceShowExpression = true; this.forceShowExpression = true;

View file

@ -14,7 +14,7 @@
> >
<MultipleParameter <MultipleParameter
:parameter="parameter" :parameter="parameter"
:values="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)" :values="getParameterValue(parameter.name)"
:node-values="nodeValues" :node-values="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@ -70,7 +70,7 @@
<CollectionParameter <CollectionParameter
v-if="parameter.type === 'collection'" v-if="parameter.type === 'collection'"
:parameter="parameter" :parameter="parameter"
:values="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)" :values="getParameterValue(parameter.name)"
:node-values="nodeValues" :node-values="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@ -79,7 +79,7 @@
<FixedCollectionParameter <FixedCollectionParameter
v-else-if="parameter.type === 'fixedCollection'" v-else-if="parameter.type === 'fixedCollection'"
:parameter="parameter" :parameter="parameter"
:values="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)" :values="getParameterValue(parameter.name)"
:node-values="nodeValues" :node-values="nodeValues"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@ -111,7 +111,7 @@
<FilterConditions <FilterConditions
v-else-if="parameter.type === 'filter'" v-else-if="parameter.type === 'filter'"
:parameter="parameter" :parameter="parameter"
:value="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)" :value="getParameterValue(parameter.name)"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:node="node" :node="node"
:read-only="isReadOnly" :read-only="isReadOnly"
@ -120,7 +120,7 @@
<AssignmentCollection <AssignmentCollection
v-else-if="parameter.type === 'assignmentCollection'" v-else-if="parameter.type === 'assignmentCollection'"
:parameter="parameter" :parameter="parameter"
:value="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)" :value="getParameterValue(parameter.name)"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:node="node" :node="node"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@ -144,7 +144,7 @@
<ParameterInputFull <ParameterInputFull
:parameter="parameter" :parameter="parameter"
:hide-issues="hiddenIssuesInputs.includes(parameter.name)" :hide-issues="hiddenIssuesInputs.includes(parameter.name)"
:value="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)" :value="getParameterValue(parameter.name)"
:display-options="shouldShowOptions(parameter)" :display-options="shouldShowOptions(parameter)"
:path="getPath(parameter.name)" :path="getPath(parameter.name)"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@ -167,6 +167,7 @@ import type {
INodeProperties, INodeProperties,
INodeTypeDescription, INodeTypeDescription,
NodeParameterValue, NodeParameterValue,
NodeParameterValueType,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { deepCopy } from 'n8n-workflow'; import { deepCopy } from 'n8n-workflow';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
@ -385,10 +386,7 @@ export default defineComponent({
return dependencies; return dependencies;
}, },
multipleValues(parameter: INodeProperties): boolean { multipleValues(parameter: INodeProperties): boolean {
if (this.getArgument('multipleValues', parameter) === true) { return this.getArgument('multipleValues', parameter) === true;
return true;
}
return false;
}, },
getArgument( getArgument(
argumentName: string, argumentName: string,
@ -460,7 +458,7 @@ export default defineComponent({
const nodeValues: INodeParameters = {}; const nodeValues: INodeParameters = {};
let rawValues = this.nodeValues; let rawValues = this.nodeValues;
if (this.path) { if (this.path) {
rawValues = get(this.nodeValues, this.path); rawValues = get(this.nodeValues, this.path) as INodeParameters;
} }
if (!rawValues) { if (!rawValues) {
@ -473,11 +471,12 @@ export default defineComponent({
let parameterGotResolved = false; let parameterGotResolved = false;
do { do {
key = resolveKeys.shift() as string; key = resolveKeys.shift() as string;
if (typeof rawValues[key] === 'string' && rawValues[key].charAt(0) === '=') { const value = rawValues[key];
if (typeof value === 'string' && value?.charAt(0) === '=') {
// Contains an expression that // Contains an expression that
if ( if (
rawValues[key].includes('$parameter') && value.includes('$parameter') &&
resolveKeys.some((parameterName) => rawValues[key].includes(parameterName)) resolveKeys.some((parameterName) => value.includes(parameterName))
) { ) {
// Contains probably an expression of a missing parameter so skip // Contains probably an expression of a missing parameter so skip
resolveKeys.push(key); resolveKeys.push(key);
@ -486,7 +485,7 @@ export default defineComponent({
// Contains probably no expression with a missing parameter so resolve // Contains probably no expression with a missing parameter so resolve
try { try {
nodeValues[key] = this.workflowHelpers.resolveExpression( nodeValues[key] = this.workflowHelpers.resolveExpression(
rawValues[key], value,
nodeValues, nodeValues,
) as NodeParameterValue; ) as NodeParameterValue;
} catch (e) { } catch (e) {
@ -574,6 +573,9 @@ export default defineComponent({
return null; return null;
} }
}, },
getParameterValue<T extends NodeParameterValueType = NodeParameterValueType>(name: string): T {
return this.nodeHelpers.getParameterValue(this.nodeValues, name, this.path) as T;
},
}, },
}); });
</script> </script>

View file

@ -50,7 +50,7 @@ import { mapStores } from 'pinia';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { INodeUi, IUpdateInformation, TargetItem } from '@/Interface'; import type { INodeUi, IUpdateInformation, InputSize, TargetItem } from '@/Interface';
import ParameterInput from '@/components/ParameterInput.vue'; import ParameterInput from '@/components/ParameterInput.vue';
import InputHint from '@/components/ParameterInputHint.vue'; import InputHint from '@/components/ParameterInputHint.vue';
import { useEnvironmentsStore } from '@/stores/environments.ee.store'; import { useEnvironmentsStore } from '@/stores/environments.ee.store';
@ -96,9 +96,11 @@ export default defineComponent({
}, },
parameter: { parameter: {
type: Object as PropType<INodeProperties>, type: Object as PropType<INodeProperties>,
required: true,
}, },
path: { path: {
type: String, type: String,
required: true,
}, },
modelValue: { modelValue: {
type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>, type: [String, Number, Boolean, Array, Object] as PropType<NodeParameterValueType>,
@ -121,7 +123,7 @@ export default defineComponent({
required: false, required: false,
}, },
inputSize: { inputSize: {
type: String, type: String as PropType<InputSize>,
}, },
hideIssues: { hideIssues: {
type: Boolean, type: Boolean,
@ -202,15 +204,14 @@ export default defineComponent({
? this.modelValue.value ? this.modelValue.value
: this.modelValue; : this.modelValue;
if ( if (!this.activeNode || !this.isValueExpression || typeof value !== 'string') {
!this.isForCredential &&
(!this.activeNode || !this.isValueExpression || typeof value !== 'string')
) {
return { ok: false, error: new Error() }; return { ok: false, error: new Error() };
} }
try { try {
let opts = { isForCredential: this.isForCredential }; let opts: Parameters<typeof this.workflowHelpers.resolveExpression>[2] = {
isForCredential: this.isForCredential,
};
if (this.ndvStore.isInputParentOfActiveNode) { if (this.ndvStore.isInputParentOfActiveNode) {
opts = { opts = {
...opts, ...opts,

View file

@ -20,7 +20,7 @@
icon-size="small" icon-size="small"
:actions="actions" :actions="actions"
:icon-orientation="iconOrientation" :icon-orientation="iconOrientation"
@action="(action) => $emit('update:modelValue', action)" @action="(action: string) => $emit('update:modelValue', action)"
@visible-change="onMenuToggle" @visible-change="onMenuToggle"
/> />
</div> </div>
@ -40,7 +40,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { NodeParameterValueType } from 'n8n-workflow'; import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { isResourceLocatorValue } from '@/utils/typeGuards'; import { isResourceLocatorValue } from '@/utils/typeGuards';
@ -51,7 +51,8 @@ export default defineComponent({
name: 'ParameterOptions', name: 'ParameterOptions',
props: { props: {
parameter: { parameter: {
type: Object, type: Object as PropType<INodeProperties>,
required: true,
}, },
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
@ -87,6 +88,7 @@ export default defineComponent({
}, },
}, },
}, },
emits: ['update:modelValue', 'menu-expanded'],
computed: { computed: {
isDefault(): boolean { isDefault(): boolean {
return this.parameter.default === this.value; return this.parameter.default === this.value;
@ -109,7 +111,7 @@ export default defineComponent({
return false; return false;
} }
if (['codeNodeEditor', 'sqlEditor'].includes(this.parameter.typeOptions?.editor)) { if (['codeNodeEditor', 'sqlEditor'].includes(this.parameter.typeOptions?.editor ?? '')) {
return false; return false;
} }

View file

@ -2,7 +2,7 @@
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { computed, defineComponent } from 'vue'; import { computed, defineComponent } from 'vue';
import { useRBACStore } from '@/stores/rbac.store'; import { useRBACStore } from '@/stores/rbac.store';
import type { HasScopeMode, Scope, Resource } from '@n8n/permissions'; import type { ScopeMode, Scope, Resource } from '@n8n/permissions';
import { import {
inferProjectIdFromRoute, inferProjectIdFromRoute,
inferResourceIdFromRoute, inferResourceIdFromRoute,
@ -17,7 +17,7 @@ export default defineComponent({
required: true, required: true,
}, },
mode: { mode: {
type: String as PropType<HasScopeMode>, type: String as PropType<ScopeMode>,
default: 'allOf', default: 'allOf',
}, },
resourceType: { resourceType: {

View file

@ -144,7 +144,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import type { IResourceLocatorReqParams, IResourceLocatorResultExpanded } from '@/Interface'; import type { DynamicNodeParameters, IResourceLocatorResultExpanded } from '@/Interface';
import DraggableTarget from '@/components/DraggableTarget.vue'; import DraggableTarget from '@/components/DraggableTarget.vue';
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'; import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
import ParameterIssues from '@/components/ParameterIssues.vue'; import ParameterIssues from '@/components/ParameterIssues.vue';
@ -197,9 +197,7 @@ export default defineComponent({
required: true, required: true,
}, },
modelValue: { modelValue: {
type: [Object, String] as PropType< type: Object as PropType<INodeParameterResourceLocator>,
INodeParameterResourceLocator | NodeParameterValue | undefined
>,
}, },
inputSize: { inputSize: {
type: String, type: String,
@ -221,8 +219,7 @@ export default defineComponent({
default: '', default: '',
}, },
expressionComputedValue: { expressionComputedValue: {
type: String, type: {} as PropType<unknown>,
default: '',
}, },
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
@ -230,6 +227,7 @@ export default defineComponent({
}, },
expressionDisplayValue: { expressionDisplayValue: {
type: String, type: String,
default: '',
}, },
forceShowExpression: { forceShowExpression: {
type: Boolean, type: Boolean,
@ -245,9 +243,11 @@ export default defineComponent({
}, },
node: { node: {
type: Object as PropType<INode>, type: Object as PropType<INode>,
required: true,
}, },
path: { path: {
type: String, type: String,
required: true,
}, },
loadOptionsMethod: { loadOptionsMethod: {
type: String, type: String,
@ -506,10 +506,10 @@ export default defineComponent({
this.width = containerRef?.offsetWidth; this.width = containerRef?.offsetWidth;
} }
}, },
getLinkAlt(entity: string) { getLinkAlt(entity: NodeParameterValue) {
if (this.selectedMode === 'list' && entity) { if (this.selectedMode === 'list' && entity) {
return this.$locale.baseText('resourceLocator.openSpecificResource', { return this.$locale.baseText('resourceLocator.openSpecificResource', {
interpolate: { entity, appName: this.appName }, interpolate: { entity: entity.toString(), appName: this.appName },
}); });
} }
return this.$locale.baseText('resourceLocator.openResource', { return this.$locale.baseText('resourceLocator.openResource', {
@ -520,7 +520,7 @@ export default defineComponent({
this.cachedResponses = {}; this.cachedResponses = {};
this.trackEvent('User refreshed resource locator list'); this.trackEvent('User refreshed resource locator list');
}, },
onKeyDown(e: MouseEvent) { onKeyDown(e: KeyboardEvent) {
if (this.resourceDropdownVisible && !this.isSearchable) { if (this.resourceDropdownVisible && !this.isSearchable) {
this.eventBus.emit('keyDown', e); this.eventBus.emit('keyDown', e);
} }
@ -555,6 +555,9 @@ export default defineComponent({
return; return;
} }
const id = node.credentials[credentialKey].id; const id = node.credentials[credentialKey].id;
if (!id) {
return;
}
this.uiStore.openExistingCredential(id); this.uiStore.openExistingCredential(id);
}, },
createNewCredential(): void { createNewCredential(): void {
@ -664,11 +667,11 @@ export default defineComponent({
return; return;
} }
let paginationToken: unknown = null; let paginationToken: string | undefined;
try { try {
if (cachedResponse) { if (cachedResponse) {
const nextPageToken = cachedResponse.nextPageToken; const nextPageToken = cachedResponse.nextPageToken as string;
if (nextPageToken) { if (nextPageToken) {
paginationToken = nextPageToken; paginationToken = nextPageToken;
this.setResponse(paramsKey, { loading: true }); this.setResponse(paramsKey, { loading: true });
@ -690,11 +693,12 @@ export default defineComponent({
this.parameter, this.parameter,
params.parameters, params.parameters,
) as INodeParameters; ) as INodeParameters;
const loadOptionsMethod = this.getPropertyArgument(this.currentMode, 'searchListMethod') as const loadOptionsMethod = this.getPropertyArgument(
| string this.currentMode,
| undefined; 'searchListMethod',
) as string;
const requestParams: IResourceLocatorReqParams = { const requestParams: DynamicNodeParameters.ResourceLocatorResultsRequest = {
nodeTypeAndVersion: { nodeTypeAndVersion: {
name: this.node.type, name: this.node.type,
version: this.node.typeVersion, version: this.node.typeVersion,
@ -703,10 +707,16 @@ export default defineComponent({
methodName: loadOptionsMethod, methodName: loadOptionsMethod,
currentNodeParameters: resolvedNodeParameters, currentNodeParameters: resolvedNodeParameters,
credentials: this.node.credentials, credentials: this.node.credentials,
...(params.filter ? { filter: params.filter } : {}),
...(paginationToken ? { paginationToken } : {}),
}; };
if (params.filter) {
requestParams.filter = params.filter;
}
if (paginationToken) {
requestParams.paginationToken = paginationToken;
}
const response = await this.nodeTypesStore.getResourceLocatorResults(requestParams); const response = await this.nodeTypesStore.getResourceLocatorResults(requestParams);
this.setResponse(paramsKey, { this.setResponse(paramsKey, {

View file

@ -41,7 +41,7 @@
> >
<div <div
v-for="(result, i) in sortedResources" v-for="(result, i) in sortedResources"
:key="result.value" :key="result.value.toString()"
:ref="`item-${i}`" :ref="`item-${i}`"
:class="{ :class="{
[$style.resourceItem]: true, [$style.resourceItem]: true,
@ -83,6 +83,7 @@ import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import type { EventBus } from 'n8n-design-system/utils'; import type { EventBus } from 'n8n-design-system/utils';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import type { NodeParameterValue } from 'n8n-workflow';
const SEARCH_BAR_HEIGHT_PX = 40; const SEARCH_BAR_HEIGHT_PX = 40;
const SCROLL_MARGIN_PX = 10; const SCROLL_MARGIN_PX = 10;
@ -91,7 +92,7 @@ export default defineComponent({
name: 'ResourceLocatorDropdown', name: 'ResourceLocatorDropdown',
props: { props: {
modelValue: { modelValue: {
type: [String, Number], type: [String, Number] as PropType<NodeParameterValue>,
}, },
show: { show: {
type: Boolean, type: Boolean,
@ -126,6 +127,7 @@ export default defineComponent({
default: () => createEventBus(), default: () => createEventBus(),
}, },
}, },
emits: ['update:modelValue', 'loadMore', 'filter'],
data() { data() {
return { return {
hoverIndex: 0, hoverIndex: 0,
@ -135,7 +137,7 @@ export default defineComponent({
computed: { computed: {
sortedResources(): IResourceLocatorResultExpanded[] { sortedResources(): IResourceLocatorResultExpanded[] {
const seen = new Set(); const seen = new Set();
const { selected, notSelected } = this.resources.reduce( const { selected, notSelected } = (this.resources ?? []).reduce(
(acc, item: IResourceLocatorResultExpanded) => { (acc, item: IResourceLocatorResultExpanded) => {
if (seen.has(item.value)) { if (seen.has(item.value)) {
return acc; return acc;
@ -232,7 +234,7 @@ export default defineComponent({
onFilterInput(value: string) { onFilterInput(value: string) {
this.$emit('filter', value); this.$emit('filter', value);
}, },
onItemClick(selected: string) { onItemClick(selected: string | number | boolean) {
this.$emit('update:modelValue', selected); this.$emit('update:modelValue', selected);
}, },
onItemHover(index: number) { onItemHover(index: number) {

View file

@ -67,7 +67,7 @@ function markAsReadOnly(field: ResourceMapperField): boolean {
return field.readOnly || false; return field.readOnly || false;
} }
const fieldsUi = computed<Array<Partial<INodeProperties> & { readOnly?: boolean }>>(() => { const fieldsUi = computed<Array<INodeProperties & { readOnly: boolean }>>(() => {
return props.fieldsToMap return props.fieldsToMap
.filter((field) => field.display && field.removed !== true) .filter((field) => field.display && field.removed !== true)
.map((field) => { .map((field) => {
@ -85,11 +85,11 @@ const fieldsUi = computed<Array<Partial<INodeProperties> & { readOnly?: boolean
}); });
}); });
const orderedFields = computed<Array<Partial<INodeProperties> & { readOnly?: boolean }>>(() => { const orderedFields = computed<Array<INodeProperties & { readOnly: boolean }>>(() => {
// Sort so that matching columns are first // Sort so that matching columns are first
if (props.paramValue.matchingColumns) { if (props.paramValue.matchingColumns) {
fieldsUi.value.forEach((field, i) => { fieldsUi.value.forEach((field, i) => {
const fieldName = parseResourceMapperFieldName(field.name); const fieldName = field.name && parseResourceMapperFieldName(field.name);
if (fieldName) { if (fieldName) {
if (props.paramValue.matchingColumns.includes(fieldName)) { if (props.paramValue.matchingColumns.includes(fieldName)) {
fieldsUi.value.splice(i, 1); fieldsUi.value.splice(i, 1);

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { IUpdateInformation, ResourceMapperReqParams } from '@/Interface'; import type { IUpdateInformation, DynamicNodeParameters } from '@/Interface';
import { resolveRequiredParameters } from '@/composables/useWorkflowHelpers'; import { resolveRequiredParameters } from '@/composables/useWorkflowHelpers';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import type { import type {
@ -37,6 +37,7 @@ const workflowsStore = useWorkflowsStore();
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
teleported: true, teleported: true,
dependentParametersValues: null,
}); });
const emit = defineEmits<{ const emit = defineEmits<{
@ -235,7 +236,13 @@ async function loadFieldsToMap(): Promise<void> {
if (!props.node) { if (!props.node) {
return; return;
} }
const requestParams: ResourceMapperReqParams = {
const methodName = props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod;
if (typeof methodName !== 'string') {
return;
}
const requestParams: DynamicNodeParameters.ResourceMapperFieldsRequest = {
nodeTypeAndVersion: { nodeTypeAndVersion: {
name: props.node?.type, name: props.node?.type,
version: props.node.typeVersion, version: props.node.typeVersion,
@ -245,7 +252,7 @@ async function loadFieldsToMap(): Promise<void> {
props.node.parameters, props.node.parameters,
) as INodeParameters, ) as INodeParameters,
path: props.path, path: props.path,
methodName: props.parameter.typeOptions?.resourceMapper?.resourceMapperMethod, methodName,
credentials: props.node.credentials, credentials: props.node.credentials,
}; };
const fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams); const fetchedFields = await nodeTypesStore.getResourceMapperFields(requestParams);
@ -310,7 +317,7 @@ function setDefaultFieldValues(forceMatchingFieldsUpdate = false): void {
function updateNodeIssues(): void { function updateNodeIssues(): void {
if (props.node) { if (props.node) {
const parameterIssues = NodeHelpers.getNodeParametersIssues( const parameterIssues = NodeHelpers.getNodeParametersIssues(
nodeType.value?.properties || [], nodeType.value?.properties ?? [],
props.node, props.node,
); );
if (parameterIssues) { if (parameterIssues) {
@ -319,10 +326,10 @@ function updateNodeIssues(): void {
} }
} }
function onMatchingColumnsChanged(matchingColumns: string[]): void { function onMatchingColumnsChanged(columns: string[]): void {
state.paramValue = { state.paramValue = {
...state.paramValue, ...state.paramValue,
matchingColumns, matchingColumns: columns,
}; };
// Set all matching fields to be visible // Set all matching fields to be visible
state.paramValue.schema.forEach((field) => { state.paramValue.schema.forEach((field) => {

View file

@ -93,13 +93,8 @@ export default defineComponent({
return; return;
} }
length += ( const value = this.modelValue[key];
Array.isArray(this.modelValue[key]) length += (Array.isArray(value) ? value.length > 0 : value !== '') ? 1 : 0;
? this.modelValue[key].length > 0
: this.modelValue[key] !== ''
)
? 1
: 0;
}); });
return length; return length;

View file

@ -2,7 +2,6 @@ import { deepCopy } from 'n8n-workflow';
import type { import type {
ExecutionError, ExecutionError,
GenericValue, GenericValue,
INodeParameters,
INodeProperties, INodeProperties,
ITelemetryTrackProperties, ITelemetryTrackProperties,
NodeParameterValue, NodeParameterValue,
@ -119,7 +118,7 @@ export interface ExpressionEditorEventsData {
dialogVisible: boolean; dialogVisible: boolean;
value: string; value: string;
resolvedExpressionValue: string; resolvedExpressionValue: string;
parameter: INodeParameters; parameter: INodeProperties;
} }
export const getExpressionEditorEventsData = ( export const getExpressionEditorEventsData = (

View file

@ -204,7 +204,7 @@ export class I18nClass {
/** /**
* Display name for an input label, whether top-level or nested. * Display name for an input label, whether top-level or nested.
*/ */
inputLabelDisplayName(parameter: INodeProperties, path: string) { inputLabelDisplayName(parameter: INodeProperties | INodePropertyCollection, path: string) {
const middleKey = deriveMiddleKey(path, parameter); const middleKey = deriveMiddleKey(path, parameter);
return context.dynamicRender({ return context.dynamicRender({

View file

@ -6,7 +6,7 @@
* *
* Location: `n8n-nodes-base.nodes.github.nodeView.<middleKey>.placeholder` * Location: `n8n-nodes-base.nodes.github.nodeView.<middleKey>.placeholder`
*/ */
export function deriveMiddleKey(path: string, parameter: { name: string; type: string }) { export function deriveMiddleKey(path: string, parameter: { name: string; type?: string }) {
let middleKey = parameter.name; let middleKey = parameter.name;
if (isTopLevelCollection(path, parameter) || isNestedInCollectionLike(path)) { if (isTopLevelCollection(path, parameter) || isNestedInCollectionLike(path)) {
@ -28,17 +28,17 @@ export function deriveMiddleKey(path: string, parameter: { name: string; type: s
*/ */
export const isNestedInCollectionLike = (path: string) => path.split('.').length >= 3; export const isNestedInCollectionLike = (path: string) => path.split('.').length >= 3;
const isTopLevelCollection = (path: string, parameter: { type: string }) => const isTopLevelCollection = (path: string, parameter: { type?: string }) =>
path.split('.').length === 2 && parameter.type === 'collection'; path.split('.').length === 2 && parameter.type === 'collection';
const isNestedCollection = (path: string, parameter: { type: string }) => const isNestedCollection = (path: string, parameter: { type?: string }) =>
path.split('.').length > 2 && parameter.type === 'collection'; path.split('.').length > 2 && parameter.type === 'collection';
/** /**
* Check if the param is a normal `fixedCollection`, i.e. a FC other than the wrapper * Check if the param is a normal `fixedCollection`, i.e. a FC other than the wrapper
* that sits at the root of a node's top-level param and contains all of them. * that sits at the root of a node's top-level param and contains all of them.
*/ */
const isFixedCollection = (path: string, parameter: { type: string }) => const isFixedCollection = (path: string, parameter: { type?: string }) =>
parameter.type === 'fixedCollection' && path !== 'parameters'; parameter.type === 'fixedCollection' && path !== 'parameters';
/** /**

View file

@ -4,7 +4,6 @@ import type {
IExecuteFunctions, IExecuteFunctions,
INode, INode,
INodeExecutionData, INodeExecutionData,
ValidationResult,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { import {
ApplicationError, ApplicationError,
@ -200,7 +199,7 @@ export const validateEntry = (
if (!validationResult.valid) { if (!validationResult.valid) {
if (ignoreErrors) { if (ignoreErrors) {
validationResult.newValue = value as ValidationResult['newValue']; return { name, value: value ?? null };
} else { } else {
const message = `${validationResult.errorMessage} [item ${itemIndex}]`; const message = `${validationResult.errorMessage} [item ${itemIndex}]`;
throw new NodeOperationError(node, message, { throw new NodeOperationError(node, message, {
@ -212,7 +211,7 @@ export const validateEntry = (
return { return {
name, name,
value: validationResult.newValue === undefined ? null : validationResult.newValue, value: validationResult.newValue ?? null,
}; };
}; };

View file

@ -1118,6 +1118,9 @@ export type NodeParameterValueType =
| NodeParameterValue | NodeParameterValue
| INodeParameters | INodeParameters
| INodeParameterResourceLocator | INodeParameterResourceLocator
| ResourceMapperValue
| FilterValue
| AssignmentCollectionValue
| NodeParameterValue[] | NodeParameterValue[]
| INodeParameters[] | INodeParameters[]
| INodeParameterResourceLocator[] | INodeParameterResourceLocator[]
@ -2374,23 +2377,30 @@ export interface ResourceMapperField {
readOnly?: boolean; readOnly?: boolean;
} }
export type FieldType = export type FieldTypeMap = {
| 'string' // eslint-disable-next-line id-denylist
| 'string-alphanumeric' boolean: boolean;
| 'number' // eslint-disable-next-line id-denylist
| 'dateTime' number: number;
| 'boolean' // eslint-disable-next-line id-denylist
| 'time' string: string;
| 'array' 'string-alphanumeric': string;
| 'object' dateTime: string;
| 'options' time: string;
| 'url' array: unknown[];
| 'jwt'; object: object;
options: any;
url: string;
jwt: string;
};
export type ValidationResult = { export type FieldType = keyof FieldTypeMap;
valid: boolean;
errorMessage?: string; export type ValidationResult<T extends FieldType = FieldType> =
newValue?: string | number | boolean | object | null | undefined; | { valid: false; errorMessage: string }
| {
valid: true;
newValue?: FieldTypeMap[T];
}; };
export type ResourceMapperValue = { export type ResourceMapperValue = {
@ -2418,9 +2428,9 @@ export interface FilterOperatorValue {
export type FilterConditionValue = { export type FilterConditionValue = {
id: string; id: string;
leftValue: unknown; leftValue: NodeParameterValue;
operator: FilterOperatorValue; operator: FilterOperatorValue;
rightValue: unknown; rightValue: NodeParameterValue;
}; };
export type FilterOptionsValue = { export type FilterOptionsValue = {

View file

@ -111,7 +111,13 @@ function parseFilterConditionValues(
}; };
} }
return { ok: true, result: { left: parsedLeftValue.newValue, right: parsedRightValue.newValue } }; return {
ok: true,
result: {
left: parsedLeftValue.valid ? parsedLeftValue.newValue : undefined,
right: parsedRightValue.valid ? parsedRightValue.newValue : undefined,
},
};
} }
function parseRegexPattern(pattern: string): RegExp { function parseRegexPattern(pattern: string): RegExp {

View file

@ -177,14 +177,21 @@ type ValidateFieldTypeOptions = Partial<{
strict: boolean; strict: boolean;
parseStrings: boolean; parseStrings: boolean;
}>; }>;
// Validates field against the schema and tries to parse it to the correct type // Validates field against the schema and tries to parse it to the correct type
export function validateFieldType<K extends FieldType>(
fieldName: string,
value: unknown,
type: K,
options?: ValidateFieldTypeOptions,
): ValidationResult<K>;
// eslint-disable-next-line complexity // eslint-disable-next-line complexity
export const validateFieldType = ( export function validateFieldType(
fieldName: string, fieldName: string,
value: unknown, value: unknown,
type: FieldType, type: FieldType,
options: ValidateFieldTypeOptions = {}, options: ValidateFieldTypeOptions = {},
): ValidationResult => { ): ValidationResult {
if (value === null || value === undefined) return { valid: true }; if (value === null || value === undefined) return { valid: true };
const strict = options.strict ?? false; const strict = options.strict ?? false;
const valueOptions = options.valueOptions ?? []; const valueOptions = options.valueOptions ?? [];
@ -308,4 +315,4 @@ export const validateFieldType = (
return { valid: true, newValue: value }; return { valid: true, newValue: value };
} }
} }
}; }