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,
WorkflowExecuteMode,
CallbackManager,
INodeParameters,
} from 'n8n-workflow';
import {
ExpressionError,
@ -2121,13 +2122,12 @@ export function cleanupParameterData(inputData: NodeParameterValueType): void {
}
if (typeof inputData === 'object') {
type Key = keyof typeof inputData;
(Object.keys(inputData) as Key[]).forEach((key) => {
const value = inputData[key];
Object.keys(inputData).forEach((key) => {
const value = (inputData as INodeParameters)[key];
if (typeof value === 'object') {
if (DateTime.isDateTime(value)) {
// Is a special luxon date so convert to string
inputData[key] = value.toString();
(inputData as INodeParameters)[key] = value.toString();
} else {
cleanupParameterData(value);
}
@ -2230,28 +2230,30 @@ const validateCollection = (
return validationResult;
}
for (const value of Array.isArray(validationResult.newValue)
? (validationResult.newValue as IDataObject[])
: [validationResult.newValue as IDataObject]) {
for (const key of Object.keys(value)) {
if (!validationMap[key]) continue;
if (validationResult.valid) {
for (const value of Array.isArray(validationResult.newValue)
? (validationResult.newValue as IDataObject[])
: [validationResult.newValue as IDataObject]) {
for (const key of Object.keys(value)) {
if (!validationMap[key]) continue;
const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, {
valueOptions: validationMap[key].options,
});
const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, {
valueOptions: validationMap[key].options,
});
if (!fieldValidationResult.valid) {
throw new ExpressionError(
`Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`,
{
description: fieldValidationResult.errorMessage,
runIndex,
itemIndex,
nodeCause: node.name,
},
);
if (!fieldValidationResult.valid) {
throw new ExpressionError(
`Invalid input for field '${validationMap[key].displayName}' inside '${propertyDescription.displayName}' in [item ${itemIndex}]`,
{
description: fieldValidationResult.errorMessage,
runIndex,
itemIndex,
nodeCause: node.name,
},
);
}
value[key] = fieldValidationResult.newValue;
}
value[key] = fieldValidationResult.newValue;
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -17,12 +17,24 @@ import type { ConditionResult, FilterOperator } from './types';
export const getFilterOperator = (key: string) =>
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;
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 = ({
@ -42,6 +54,7 @@ export const handleOperatorChange = ({
if (leftTypeChanged && !isExpression(condition.leftValue)) {
condition.leftValue = convertToType(condition.leftValue, newOperator.type);
}
if (rightTypeChanged && !newOperator.singleValue && !isExpression(condition.rightValue)) {
condition.rightValue = convertToType(condition.rightValue, newRightType);
}

View file

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

View file

@ -102,7 +102,7 @@ export default defineComponent({
},
props: {
nodeValues: {
type: Object as PropType<Record<string, INodeParameters[]>>,
type: Object as PropType<INodeParameters>,
required: true,
},
parameter: {
@ -159,7 +159,7 @@ export default defineComponent({
methods: {
addItem() {
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));

View file

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

View file

@ -1,6 +1,8 @@
<template>
<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">
<div
:class="$style.editContainer"
@ -39,56 +41,47 @@
</span>
</template>
<script lang="ts">
<script setup lang="ts">
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({
name: 'NodeTitle',
components: {
NodeIcon,
},
props: {
modelValue: {
type: String,
default: '',
},
nodeType: {},
readOnly: {
type: Boolean,
default: false,
},
},
data() {
return {
editName: false,
newName: '',
};
},
computed: {
editable(): boolean {
return !this.readOnly && window === window.parent;
},
},
methods: {
async onEdit() {
this.newName = this.modelValue;
this.editName = true;
await this.$nextTick();
const inputRef = this.$refs.input as HTMLInputElement | undefined;
if (inputRef) {
inputRef.focus();
}
},
onRename() {
if (this.newName.trim() !== '') {
this.$emit('update:modelValue', this.newName.trim());
}
type Props = {
modelValue: string;
nodeType?: INodeTypeDescription;
readOnly?: boolean;
};
this.editName = false;
},
},
const props = withDefaults(defineProps<Props>(), {
modelValue: '',
nodeType: undefined,
readOnly: false,
});
const emit = defineEmits<{
(event: 'update:model-value', value: string): void;
}>();
const editName = ref(false);
const newName = ref('');
const input = ref<HTMLInputElement>();
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();
}
}
function onRename() {
if (newName.value.trim() !== '') {
emit('update:model-value', newName.value.trim());
}
editName.value = false;
}
</script>
<style lang="scss" module>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -67,7 +67,7 @@ function markAsReadOnly(field: ResourceMapperField): boolean {
return field.readOnly || false;
}
const fieldsUi = computed<Array<Partial<INodeProperties> & { readOnly?: boolean }>>(() => {
const fieldsUi = computed<Array<INodeProperties & { readOnly: boolean }>>(() => {
return props.fieldsToMap
.filter((field) => field.display && field.removed !== true)
.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
if (props.paramValue.matchingColumns) {
fieldsUi.value.forEach((field, i) => {
const fieldName = parseResourceMapperFieldName(field.name);
const fieldName = field.name && parseResourceMapperFieldName(field.name);
if (fieldName) {
if (props.paramValue.matchingColumns.includes(fieldName)) {
fieldsUi.value.splice(i, 1);

View file

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

View file

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

View file

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

View file

@ -204,7 +204,7 @@ export class I18nClass {
/**
* 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);
return context.dynamicRender({

View file

@ -6,7 +6,7 @@
*
* 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;
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;
const isTopLevelCollection = (path: string, parameter: { type: string }) =>
const isTopLevelCollection = (path: string, parameter: { type?: string }) =>
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';
/**
* 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.
*/
const isFixedCollection = (path: string, parameter: { type: string }) =>
const isFixedCollection = (path: string, parameter: { type?: string }) =>
parameter.type === 'fixedCollection' && path !== 'parameters';
/**

View file

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

View file

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

View file

@ -177,14 +177,21 @@ type ValidateFieldTypeOptions = Partial<{
strict: boolean;
parseStrings: boolean;
}>;
// 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
export const validateFieldType = (
export function validateFieldType(
fieldName: string,
value: unknown,
type: FieldType,
options: ValidateFieldTypeOptions = {},
): ValidationResult => {
): ValidationResult {
if (value === null || value === undefined) return { valid: true };
const strict = options.strict ?? false;
const valueOptions = options.valueOptions ?? [];
@ -308,4 +315,4 @@ export const validateFieldType = (
return { valid: true, newValue: value };
}
}
};
}