mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 00:24:07 -08:00
refactor(editor): Migrate FixedCollectionParameter
to composition API
This commit is contained in:
parent
c5191e697a
commit
88012c172a
|
@ -0,0 +1,83 @@
|
||||||
|
import { renderComponent } from '@/__tests__/render';
|
||||||
|
import FixedCollectionParameter from './FixedCollectionParameter.vue';
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import { createAppModals } from '@/__tests__/utils';
|
||||||
|
|
||||||
|
describe('FixedCollectionParameter', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createAppModals();
|
||||||
|
const pinia = createPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders default options correctly', () => {
|
||||||
|
const { html } = renderComponent(FixedCollectionParameter, {
|
||||||
|
global: {
|
||||||
|
stubs: ['ParameterInputList'],
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
parameter: {
|
||||||
|
displayName: 'Categories',
|
||||||
|
name: 'categories',
|
||||||
|
placeholder: 'Add Category',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
default: {},
|
||||||
|
typeOptions: { multipleValues: true },
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'categories',
|
||||||
|
displayName: 'Categories',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Category',
|
||||||
|
name: 'category',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'Category to add',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Description',
|
||||||
|
name: 'description',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: "Describe your category if it's not obvious",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
nodeValues: {
|
||||||
|
color: '#ff0000',
|
||||||
|
alwaysOutputData: false,
|
||||||
|
executeOnce: false,
|
||||||
|
notesInFlow: false,
|
||||||
|
onError: 'stopWorkflow',
|
||||||
|
retryOnFail: false,
|
||||||
|
maxTries: 3,
|
||||||
|
waitBetweenTries: 1000,
|
||||||
|
notes: '',
|
||||||
|
parameters: {
|
||||||
|
inputText: '',
|
||||||
|
categories: {
|
||||||
|
categories: [
|
||||||
|
{ category: 'One', description: 'Category one' },
|
||||||
|
{ category: 'Two', description: 'New Category two' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
path: 'parameters.categories',
|
||||||
|
values: {
|
||||||
|
categories: [
|
||||||
|
{ category: 'One', description: 'Category one' },
|
||||||
|
{ category: 'Two', description: 'New Category two' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
isReadonly: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log(html);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,214 +1,199 @@
|
||||||
<script lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineComponent } from 'vue';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import type { PropType } from 'vue';
|
import { onMounted, watch } from 'vue';
|
||||||
|
import { deepCopy, isINodePropertyCollectionList } from 'n8n-workflow';
|
||||||
|
import type { INodeParameters, INodeProperties, INodePropertyCollection } from 'n8n-workflow';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import { get } from 'lodash-es';
|
||||||
import type { IUpdateInformation } from '@/Interface';
|
import type { IUpdateInformation } from '@/Interface';
|
||||||
|
|
||||||
import type { INodeParameters, INodeProperties, INodePropertyCollection } from 'n8n-workflow';
|
interface Props {
|
||||||
import { deepCopy, isINodePropertyCollectionList } from 'n8n-workflow';
|
nodeValues: INodeParameters;
|
||||||
|
parameter: INodeProperties;
|
||||||
|
path: string;
|
||||||
|
values?: Record<string, INodeParameters[]>;
|
||||||
|
isReadOnly?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
import { get } from 'lodash-es';
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
values: () => ({}),
|
||||||
export default defineComponent({
|
isReadOnly: false,
|
||||||
name: 'FixedCollectionParameter',
|
|
||||||
props: {
|
|
||||||
nodeValues: {
|
|
||||||
type: Object as PropType<INodeParameters>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
parameter: {
|
|
||||||
type: Object as PropType<INodeProperties>,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
values: {
|
|
||||||
type: Object as PropType<Record<string, INodeParameters[]>>,
|
|
||||||
default: () => ({}),
|
|
||||||
},
|
|
||||||
isReadOnly: {
|
|
||||||
type: Boolean,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
selectedOption: undefined,
|
|
||||||
mutableValues: {} as Record<string, INodeParameters[]>,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
getPlaceholderText(): string {
|
|
||||||
const placeholder = this.$locale.nodeText().placeholder(this.parameter, this.path);
|
|
||||||
return placeholder ? placeholder : this.$locale.baseText('fixedCollectionParameter.choose');
|
|
||||||
},
|
|
||||||
getProperties(): INodePropertyCollection[] {
|
|
||||||
const returnProperties = [];
|
|
||||||
let tempProperties;
|
|
||||||
for (const name of this.propertyNames) {
|
|
||||||
tempProperties = this.getOptionProperties(name);
|
|
||||||
if (tempProperties !== undefined) {
|
|
||||||
returnProperties.push(tempProperties);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return returnProperties;
|
|
||||||
},
|
|
||||||
multipleValues(): boolean {
|
|
||||||
return !!this.parameter.typeOptions?.multipleValues;
|
|
||||||
},
|
|
||||||
parameterOptions(): INodePropertyCollection[] {
|
|
||||||
if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) {
|
|
||||||
return this.parameter.options;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (this.parameter.options as INodePropertyCollection[]).filter((option) => {
|
|
||||||
return !this.propertyNames.includes(option.name);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
propertyNames(): string[] {
|
|
||||||
return Object.keys(this.mutableValues || {});
|
|
||||||
},
|
|
||||||
sortable(): boolean {
|
|
||||||
return !!this.parameter.typeOptions?.sortable;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
values: {
|
|
||||||
handler(newValues: Record<string, INodeParameters[]>) {
|
|
||||||
this.mutableValues = deepCopy(newValues);
|
|
||||||
},
|
|
||||||
deep: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
created() {
|
|
||||||
this.mutableValues = deepCopy(this.values);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
deleteOption(optionName: string, index?: number) {
|
|
||||||
const currentOptionsOfSameType = this.mutableValues[optionName];
|
|
||||||
if (!currentOptionsOfSameType || currentOptionsOfSameType.length > 1) {
|
|
||||||
// it's not the only option of this type, so just remove it.
|
|
||||||
this.$emit('valueChanged', {
|
|
||||||
name: this.getPropertyPath(optionName, index),
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// it's the only option, so remove the whole type
|
|
||||||
this.$emit('valueChanged', {
|
|
||||||
name: this.getPropertyPath(optionName),
|
|
||||||
value: undefined,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
getPropertyPath(name: string, index?: number) {
|
|
||||||
return `${this.path}.${name}` + (index !== undefined ? `[${index}]` : '');
|
|
||||||
},
|
|
||||||
getOptionProperties(optionName: string): INodePropertyCollection | undefined {
|
|
||||||
if (isINodePropertyCollectionList(this.parameter.options)) {
|
|
||||||
for (const option of this.parameter.options) {
|
|
||||||
if (option.name === optionName) {
|
|
||||||
return option;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
},
|
|
||||||
moveOptionDown(optionName: string, index: number) {
|
|
||||||
if (Array.isArray(this.mutableValues[optionName])) {
|
|
||||||
this.mutableValues[optionName].splice(
|
|
||||||
index + 1,
|
|
||||||
0,
|
|
||||||
this.mutableValues[optionName].splice(index, 1)[0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parameterData = {
|
|
||||||
name: this.getPropertyPath(optionName),
|
|
||||||
value: this.mutableValues[optionName],
|
|
||||||
type: 'optionsOrderChanged',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('valueChanged', parameterData);
|
|
||||||
},
|
|
||||||
moveOptionUp(optionName: string, index: number) {
|
|
||||||
if (Array.isArray(this.mutableValues[optionName])) {
|
|
||||||
this.mutableValues?.[optionName].splice(
|
|
||||||
index - 1,
|
|
||||||
0,
|
|
||||||
this.mutableValues[optionName].splice(index, 1)[0],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parameterData = {
|
|
||||||
name: this.getPropertyPath(optionName),
|
|
||||||
value: this.mutableValues[optionName],
|
|
||||||
type: 'optionsOrderChanged',
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('valueChanged', parameterData);
|
|
||||||
},
|
|
||||||
optionSelected(optionName: string) {
|
|
||||||
const option = this.getOptionProperties(optionName);
|
|
||||||
if (option === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const name = `${this.path}.${option.name}`;
|
|
||||||
|
|
||||||
const newParameterValue: INodeParameters = {};
|
|
||||||
|
|
||||||
for (const optionParameter of option.values) {
|
|
||||||
if (
|
|
||||||
optionParameter.type === 'fixedCollection' &&
|
|
||||||
optionParameter.typeOptions !== undefined &&
|
|
||||||
optionParameter.typeOptions.multipleValues === true
|
|
||||||
) {
|
|
||||||
newParameterValue[optionParameter.name] = {};
|
|
||||||
} else if (
|
|
||||||
optionParameter.typeOptions !== undefined &&
|
|
||||||
optionParameter.typeOptions.multipleValues === true
|
|
||||||
) {
|
|
||||||
// Multiple values are allowed so append option to array
|
|
||||||
const multiValue = get(this.nodeValues, [this.path, optionParameter.name], []);
|
|
||||||
|
|
||||||
if (Array.isArray(optionParameter.default)) {
|
|
||||||
multiValue.push(...deepCopy(optionParameter.default));
|
|
||||||
} else if (
|
|
||||||
optionParameter.default !== '' &&
|
|
||||||
typeof optionParameter.default !== 'object'
|
|
||||||
) {
|
|
||||||
multiValue.push(deepCopy(optionParameter.default));
|
|
||||||
}
|
|
||||||
|
|
||||||
newParameterValue[optionParameter.name] = multiValue;
|
|
||||||
} else {
|
|
||||||
// Add a new option
|
|
||||||
newParameterValue[optionParameter.name] = deepCopy(optionParameter.default);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let newValue;
|
|
||||||
if (this.multipleValues) {
|
|
||||||
newValue = get(this.nodeValues, name, []) as INodeParameters[];
|
|
||||||
|
|
||||||
newValue.push(newParameterValue);
|
|
||||||
} else {
|
|
||||||
newValue = newParameterValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parameterData = {
|
|
||||||
name,
|
|
||||||
value: newValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
this.$emit('valueChanged', parameterData);
|
|
||||||
this.selectedOption = undefined;
|
|
||||||
},
|
|
||||||
valueChanged(parameterData: IUpdateInformation) {
|
|
||||||
this.$emit('valueChanged', parameterData);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
valueChanged: [value: IUpdateInformation];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const selectedOption = ref<string | undefined>(undefined);
|
||||||
|
const mutableValues = ref<Record<string, INodeParameters[]>>(deepCopy(props.values));
|
||||||
|
|
||||||
|
const placeholderText = computed(() => {
|
||||||
|
const placeholder = i18n.nodeText().placeholder(props.parameter, props.path);
|
||||||
|
return placeholder ? placeholder : i18n.baseText('fixedCollectionParameter.choose');
|
||||||
|
});
|
||||||
|
|
||||||
|
const getProperties = computed(() => {
|
||||||
|
const returnProperties = [];
|
||||||
|
let tempProperties;
|
||||||
|
for (const name of propertyNames.value) {
|
||||||
|
tempProperties = getOptionProperties(name);
|
||||||
|
if (tempProperties !== undefined) {
|
||||||
|
returnProperties.push(tempProperties);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnProperties;
|
||||||
|
});
|
||||||
|
|
||||||
|
const parameterOptions = computed(() => {
|
||||||
|
if (multipleValues.value && isINodePropertyCollectionList(props.parameter.options)) {
|
||||||
|
return props.parameter.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (props.parameter.options as INodePropertyCollection[]).filter((option) => {
|
||||||
|
return !propertyNames.value.includes(option.name);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const multipleValues = computed(() => !!props.parameter.typeOptions?.multipleValues);
|
||||||
|
const propertyNames = computed(() => Object.keys(mutableValues.value || {}));
|
||||||
|
const sortable = computed(() => !!props.parameter.typeOptions?.sortable);
|
||||||
|
|
||||||
|
// TODO: Test this
|
||||||
|
watch(
|
||||||
|
() => props.values,
|
||||||
|
(newValues: Record<string, INodeParameters[]>) => {
|
||||||
|
mutableValues.value = deepCopy(newValues);
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
const deleteOption = (optionName: string, index?: number) => {
|
||||||
|
const currentOptionsOfSameType = mutableValues.value[optionName];
|
||||||
|
if (!currentOptionsOfSameType || currentOptionsOfSameType.length > 1) {
|
||||||
|
// it's not the only option of this type, so just remove it.
|
||||||
|
emit('valueChanged', {
|
||||||
|
name: getPropertyPath(optionName, index),
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// it's the only option, so remove the whole type
|
||||||
|
emit('valueChanged', {
|
||||||
|
name: getPropertyPath(optionName),
|
||||||
|
value: undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPropertyPath = (name: string, index?: number) => {
|
||||||
|
return `${props.path}.${name}` + (index !== undefined ? `[${index}]` : '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOptionProperties = (optionName: string): INodePropertyCollection | undefined => {
|
||||||
|
if (isINodePropertyCollectionList(props.parameter.options)) {
|
||||||
|
for (const option of props.parameter.options) {
|
||||||
|
if (option.name === optionName) {
|
||||||
|
return option;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveOptionDown = (optionName: string, index: number) => {
|
||||||
|
if (Array.isArray(mutableValues.value[optionName])) {
|
||||||
|
mutableValues.value[optionName].splice(
|
||||||
|
index + 1,
|
||||||
|
0,
|
||||||
|
mutableValues.value[optionName].splice(index, 1)[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterData: IUpdateInformation = {
|
||||||
|
name: getPropertyPath(optionName),
|
||||||
|
value: mutableValues.value[optionName],
|
||||||
|
type: 'optionsOrderChanged',
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('valueChanged', parameterData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const moveOptionUp = (optionName: string, index: number) => {
|
||||||
|
if (Array.isArray(mutableValues.value[optionName])) {
|
||||||
|
mutableValues.value?.[optionName].splice(
|
||||||
|
index - 1,
|
||||||
|
0,
|
||||||
|
mutableValues.value[optionName].splice(index, 1)[0],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterData: IUpdateInformation = {
|
||||||
|
name: getPropertyPath(optionName),
|
||||||
|
value: mutableValues.value[optionName],
|
||||||
|
type: 'optionsOrderChanged',
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('valueChanged', parameterData);
|
||||||
|
};
|
||||||
|
|
||||||
|
const optionSelected = (optionName: string) => {
|
||||||
|
const option = getOptionProperties(optionName);
|
||||||
|
if (option === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const name = `${props.path}.${option.name}`;
|
||||||
|
|
||||||
|
const newParameterValue: INodeParameters = {};
|
||||||
|
|
||||||
|
for (const optionParameter of option.values) {
|
||||||
|
if (
|
||||||
|
optionParameter.type === 'fixedCollection' &&
|
||||||
|
optionParameter.typeOptions !== undefined &&
|
||||||
|
optionParameter.typeOptions.multipleValues === true
|
||||||
|
) {
|
||||||
|
newParameterValue[optionParameter.name] = {};
|
||||||
|
} else if (
|
||||||
|
optionParameter.typeOptions !== undefined &&
|
||||||
|
optionParameter.typeOptions.multipleValues === true
|
||||||
|
) {
|
||||||
|
// Multiple values are allowed so append option to array
|
||||||
|
const multiValue = get(props.nodeValues, [props.path, optionParameter.name], []);
|
||||||
|
|
||||||
|
if (Array.isArray(optionParameter.default)) {
|
||||||
|
multiValue.push(...deepCopy(optionParameter.default));
|
||||||
|
} else if (optionParameter.default !== '' && typeof optionParameter.default !== 'object') {
|
||||||
|
multiValue.push(deepCopy(optionParameter.default));
|
||||||
|
}
|
||||||
|
|
||||||
|
newParameterValue[optionParameter.name] = multiValue;
|
||||||
|
} else {
|
||||||
|
// Add a new option
|
||||||
|
newParameterValue[optionParameter.name] = deepCopy(optionParameter.default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let newValue;
|
||||||
|
if (multipleValues.value) {
|
||||||
|
newValue = get(props.nodeValues, name, []) as INodeParameters[];
|
||||||
|
|
||||||
|
newValue.push(newParameterValue);
|
||||||
|
} else {
|
||||||
|
newValue = newParameterValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameterData = {
|
||||||
|
name,
|
||||||
|
value: newValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
emit('valueChanged', parameterData);
|
||||||
|
selectedOption.value = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
const valueChanged = (parameterData: IUpdateInformation) => {
|
||||||
|
emit('valueChanged', parameterData);
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -316,13 +301,13 @@ export default defineComponent({
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
block
|
block
|
||||||
data-test-id="fixed-collection-add"
|
data-test-id="fixed-collection-add"
|
||||||
:label="getPlaceholderText"
|
:label="placeholderText"
|
||||||
@click="optionSelected(parameter.options[0].name)"
|
@click="optionSelected(parameter.options[0].name)"
|
||||||
/>
|
/>
|
||||||
<div v-else class="add-option">
|
<div v-else class="add-option">
|
||||||
<n8n-select
|
<n8n-select
|
||||||
v-model="selectedOption"
|
v-model="selectedOption"
|
||||||
:placeholder="getPlaceholderText"
|
:placeholder="placeholderText"
|
||||||
size="small"
|
size="small"
|
||||||
filterable
|
filterable
|
||||||
@update:model-value="optionSelected"
|
@update:model-value="optionSelected"
|
||||||
|
|
Loading…
Reference in a new issue