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">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
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 { INodeParameters, INodeProperties, INodePropertyCollection } from 'n8n-workflow';
|
||||
import { deepCopy, isINodePropertyCollectionList } from 'n8n-workflow';
|
||||
interface Props {
|
||||
nodeValues: INodeParameters;
|
||||
parameter: INodeProperties;
|
||||
path: string;
|
||||
values?: Record<string, INodeParameters[]>;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
import { get } from 'lodash-es';
|
||||
|
||||
export default defineComponent({
|
||||
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 props = withDefaults(defineProps<Props>(), {
|
||||
values: () => ({}),
|
||||
isReadOnly: false,
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
|
@ -316,13 +301,13 @@ export default defineComponent({
|
|||
type="tertiary"
|
||||
block
|
||||
data-test-id="fixed-collection-add"
|
||||
:label="getPlaceholderText"
|
||||
:label="placeholderText"
|
||||
@click="optionSelected(parameter.options[0].name)"
|
||||
/>
|
||||
<div v-else class="add-option">
|
||||
<n8n-select
|
||||
v-model="selectedOption"
|
||||
:placeholder="getPlaceholderText"
|
||||
:placeholder="placeholderText"
|
||||
size="small"
|
||||
filterable
|
||||
@update:model-value="optionSelected"
|
||||
|
|
Loading…
Reference in a new issue