mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat: Add assignment component with drag and drop to Set node (#8283)
Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
parent
c04f92f7fd
commit
2799de491b
|
@ -188,9 +188,9 @@ describe('Data pinning', () => {
|
|||
|
||||
function setExpressionOnStringValueInSet(expression: string) {
|
||||
cy.get('button').contains('Test step').click();
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||
|
||||
ndv.getters.nthParam(4).contains('Expression').invoke('show').click();
|
||||
ndv.getters.assignmentCollectionAdd('assignments').click();
|
||||
ndv.getters.assignmentValue('assignments').contains('Expression').invoke('show').click();
|
||||
|
||||
ndv.getters
|
||||
.inlineExpressionEditorInput()
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { WorkflowPage, NDV } from '../pages';
|
||||
import { getVisibleSelect } from '../utils';
|
||||
|
||||
const wf = new WorkflowPage();
|
||||
const ndv = new NDV();
|
||||
|
@ -104,8 +105,6 @@ describe('Data transformation expressions', () => {
|
|||
|
||||
const addEditFields = () => {
|
||||
wf.actions.addNodeToCanvas('Edit Fields', true, true);
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||
ndv.getters.parameterInput('include').click(); // shorten output
|
||||
cy.get('div').contains('No Input Fields').click();
|
||||
ndv.getters.nthParam(4).contains('Expression').invoke('show').click();
|
||||
ndv.getters.assignmentCollectionAdd('assignments').click();
|
||||
ndv.getters.assignmentValue('assignments').contains('Expression').invoke('show').click();
|
||||
};
|
||||
|
|
|
@ -181,9 +181,10 @@ describe('Webhook Trigger node', async () => {
|
|||
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||
ndv.getters.nthParam(2).type('data');
|
||||
ndv.getters.nthParam(4).invoke('val', cowBase64).trigger('blur');
|
||||
ndv.getters.assignmentCollectionAdd('assignments').click();
|
||||
ndv.getters.assignmentName('assignments').type('data');
|
||||
ndv.getters.assignmentType('assignments').click();
|
||||
ndv.getters.assignmentValue('assignments').paste(cowBase64);
|
||||
|
||||
ndv.getters.backToCanvas().click();
|
||||
|
||||
|
@ -311,9 +312,9 @@ describe('Webhook Trigger node', async () => {
|
|||
const addEditFields = () => {
|
||||
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME);
|
||||
workflowPage.actions.openNode(EDIT_FIELDS_SET_NODE_NAME);
|
||||
cy.get('.fixed-collection-parameter > :nth-child(2) > .button > span').click();
|
||||
ndv.getters.nthParam(2).type('MyValue');
|
||||
ndv.getters.nthParam(3).click();
|
||||
cy.get('div').contains('Number').click();
|
||||
ndv.getters.nthParam(4).type('1234');
|
||||
ndv.getters.assignmentCollectionAdd('assignments').click();
|
||||
ndv.getters.assignmentName('assignments').type('MyValue');
|
||||
ndv.getters.assignmentType('assignments').click();
|
||||
getVisibleSelect().find('li').contains('Number').click();
|
||||
ndv.getters.assignmentValue('assignments').type('1234');
|
||||
};
|
||||
|
|
|
@ -306,7 +306,7 @@ describe('NDV', () => {
|
|||
|
||||
ndv.getters.parameterInput('remoteOptions').click();
|
||||
|
||||
ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false});
|
||||
ndv.getters.parameterInputIssues('remoteOptions').realHover({ scrollBehavior: false });
|
||||
// Remote options dropdown should not be visible
|
||||
ndv.getters.parameterInput('remoteOptions').find('.el-select').should('not.exist');
|
||||
});
|
||||
|
@ -509,12 +509,12 @@ describe('NDV', () => {
|
|||
|
||||
workflowPage.actions.openNode('Edit Fields (old)');
|
||||
ndv.actions.openSettings();
|
||||
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.2)');
|
||||
ndv.getters.nodeVersion().should('have.text', 'Set node version 2 (Latest version: 3.3)');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Edit Fields (latest)');
|
||||
ndv.actions.openSettings();
|
||||
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.2 (Latest)');
|
||||
ndv.getters.nodeVersion().should('have.text', 'Edit Fields (Set) node version 3.3 (Latest)');
|
||||
ndv.actions.close();
|
||||
|
||||
workflowPage.actions.openNode('Function');
|
||||
|
|
|
@ -37,7 +37,7 @@
|
|||
"id": "93aaadac-55fe-4618-b1eb-f63e61d1446a",
|
||||
"name": "Edit Fields (latest)",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.2,
|
||||
"typeVersion": 3.3,
|
||||
"position": [
|
||||
1720,
|
||||
780
|
||||
|
|
|
@ -94,6 +94,20 @@ export class NDV extends BasePage {
|
|||
this.getters.filterComponent(paramName).getByTestId('filter-remove-condition').eq(index),
|
||||
filterConditionAdd: (paramName: string) =>
|
||||
this.getters.filterComponent(paramName).getByTestId('filter-add-condition'),
|
||||
assignmentCollection: (paramName: string) =>
|
||||
cy.getByTestId(`assignment-collection-${paramName}`),
|
||||
assignmentCollectionAdd: (paramName: string) =>
|
||||
this.getters.assignmentCollection(paramName).getByTestId('assignment-collection-drop-area'),
|
||||
assignment: (paramName: string, index = 0) =>
|
||||
this.getters.assignmentCollection(paramName).getByTestId('assignment').eq(index),
|
||||
assignmentRemove: (paramName: string, index = 0) =>
|
||||
this.getters.assignment(paramName, index).getByTestId('assignment-remove'),
|
||||
assignmentName: (paramName: string, index = 0) =>
|
||||
this.getters.assignment(paramName, index).getByTestId('assignment-name'),
|
||||
assignmentValue: (paramName: string, index = 0) =>
|
||||
this.getters.assignment(paramName, index).getByTestId('assignment-value'),
|
||||
assignmentType: (paramName: string, index = 0) =>
|
||||
this.getters.assignment(paramName, index).getByTestId('assignment-type-select'),
|
||||
searchInput: () => cy.getByTestId('ndv-search'),
|
||||
pagination: () => cy.getByTestId('ndv-data-pagination'),
|
||||
nodeVersion: () => cy.getByTestId('node-version'),
|
||||
|
@ -235,6 +249,9 @@ export class NDV extends BasePage {
|
|||
removeFilterCondition: (paramName: string, index: number) => {
|
||||
this.getters.filterConditionRemove(paramName, index).click();
|
||||
},
|
||||
removeAssignment: (paramName: string, index: number) => {
|
||||
this.getters.assignmentRemove(paramName, index).click();
|
||||
},
|
||||
setInvalidExpression: ({
|
||||
fieldName,
|
||||
invalidExpression,
|
||||
|
|
|
@ -46,6 +46,12 @@
|
|||
var(--prim-color-primary-s),
|
||||
var(--prim-color-primary-l)
|
||||
);
|
||||
--prim-color-primary-alpha-010: hsla(
|
||||
var(--prim-color-primary-h),
|
||||
var(--prim-color-primary-s),
|
||||
var(--prim-color-primary-l),
|
||||
0.1
|
||||
);
|
||||
--prim-color-primary-tint-100: hsl(
|
||||
var(--prim-color-primary-h),
|
||||
var(--prim-color-primary-s),
|
||||
|
@ -93,6 +99,12 @@
|
|||
var(--prim-color-secondary-l),
|
||||
0.25
|
||||
);
|
||||
--prim-color-secondary-alpha-010: hsla(
|
||||
var(--prim-color-secondary-h),
|
||||
var(--prim-color-secondary-s),
|
||||
var(--prim-color-secondary-l),
|
||||
0.1
|
||||
);
|
||||
--prim-color-secondary-tint-100: hsl(
|
||||
var(--prim-color-secondary-h),
|
||||
var(--prim-color-secondary-s),
|
||||
|
@ -140,6 +152,12 @@
|
|||
var(--prim-color-alt-a-l),
|
||||
0.25
|
||||
);
|
||||
--prim-color-alt-a-alpha-015: hsl(
|
||||
var(--prim-color-alt-a-h),
|
||||
var(--prim-color-alt-a-s),
|
||||
var(--prim-color-alt-a-l),
|
||||
0.15
|
||||
);
|
||||
--prim-color-alt-a-tint-300: hsl(
|
||||
var(--prim-color-alt-a-h),
|
||||
var(--prim-color-alt-a-s),
|
||||
|
|
|
@ -131,6 +131,8 @@
|
|||
// NDV
|
||||
--color-run-data-background: var(--prim-gray-800);
|
||||
--color-ndv-droppable-parameter: var(--prim-color-primary);
|
||||
--color-ndv-droppable-parameter-background: var(--prim-color-primary-alpha-010);
|
||||
--color-ndv-droppable-parameter-active-background: var(--prim-color-alt-a-alpha-015);
|
||||
--color-ndv-back-font: var(--prim-gray-0);
|
||||
--color-ndv-ouptut-error-font: var(--prim-color-alt-c-tint-150);
|
||||
|
||||
|
@ -174,6 +176,9 @@
|
|||
// Action Dropdown
|
||||
--color-action-dropdown-item-active-background: var(--color-background-xlight);
|
||||
|
||||
// Input Triple
|
||||
--color-background-input-triple: var(--prim-gray-800);
|
||||
|
||||
// Various
|
||||
--color-info-tint-1: var(--prim-gray-420);
|
||||
--color-info-tint-2: var(--prim-gray-740);
|
||||
|
|
|
@ -201,6 +201,8 @@
|
|||
// NDV
|
||||
--color-run-data-background: var(--color-background-base);
|
||||
--color-ndv-droppable-parameter: var(--color-secondary);
|
||||
--color-ndv-droppable-parameter-background: var(--prim-color-secondary-alpha-010);
|
||||
--color-ndv-droppable-parameter-active-background: var(--prim-color-alt-a-alpha-015);
|
||||
--color-ndv-back-font: var(--prim-gray-0);
|
||||
--color-ndv-ouptut-error-font: var(--prim-color-alt-c);
|
||||
|
||||
|
@ -253,6 +255,9 @@
|
|||
// Feature Request
|
||||
--color-feature-request-font: var(--prim-gray-0);
|
||||
|
||||
// Input Triple
|
||||
--color-background-input-triple: var(--color-background-light);
|
||||
|
||||
// Various
|
||||
--color-avatar-accent-1: var(--prim-gray-120);
|
||||
--color-avatar-accent-2: var(--prim-color-alt-e-shade-100);
|
||||
|
|
|
@ -96,7 +96,7 @@
|
|||
}
|
||||
|
||||
@include mixins.m(small) {
|
||||
line-height: #{var.$input-small-height - 2};
|
||||
line-height: #{var.$input-small-height - 4};
|
||||
|
||||
@include mixins.e((increase, decrease)) {
|
||||
width: var.$input-small-height;
|
||||
|
|
|
@ -1235,8 +1235,8 @@ export interface NDVState {
|
|||
isDragging: boolean;
|
||||
type: string;
|
||||
data: string;
|
||||
activeTargetId: string | null;
|
||||
stickyPosition: null | XYPosition;
|
||||
dimensions: DOMRect | null;
|
||||
activeTarget: { id: string; stickyPosition: null | XYPosition } | null;
|
||||
};
|
||||
isMappingOnboarded: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
<script setup lang="ts">
|
||||
import type { IUpdateInformation } from '@/Interface';
|
||||
import InputTriple from '@/components/InputTriple/InputTriple.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import ParameterInputHint from '@/components/ParameterInputHint.vue';
|
||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||
import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||
import { isExpression } from '@/utils/expressions';
|
||||
import { isObject } from '@jsplumb/util';
|
||||
import type { AssignmentValue, INodeProperties } from 'n8n-workflow';
|
||||
import { computed, ref } from 'vue';
|
||||
import TypeSelect from './TypeSelect.vue';
|
||||
|
||||
interface Props {
|
||||
path: string;
|
||||
modelValue: AssignmentValue;
|
||||
issues: string[];
|
||||
hideType?: boolean;
|
||||
isReadOnly?: boolean;
|
||||
index?: number;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const assignment = ref<AssignmentValue>(props.modelValue);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:model-value', value: AssignmentValue): void;
|
||||
(event: 'remove'): void;
|
||||
}>();
|
||||
|
||||
const assignmentTypeToNodeProperty = (
|
||||
type: string,
|
||||
): Partial<INodeProperties> & Pick<INodeProperties, 'type'> => {
|
||||
switch (type) {
|
||||
case 'boolean':
|
||||
return {
|
||||
type: 'options',
|
||||
default: false,
|
||||
options: [
|
||||
{ name: 'false', value: false },
|
||||
{ name: 'true', value: true },
|
||||
],
|
||||
};
|
||||
case 'array':
|
||||
case 'object':
|
||||
case 'any':
|
||||
return { type: 'string' };
|
||||
default:
|
||||
return { type } as INodeProperties;
|
||||
}
|
||||
};
|
||||
|
||||
const nameParameter = computed<INodeProperties>(() => ({
|
||||
name: '',
|
||||
displayName: '',
|
||||
default: '',
|
||||
requiresDataPath: 'single',
|
||||
placeholder: 'name',
|
||||
type: 'string',
|
||||
}));
|
||||
|
||||
const valueParameter = computed<INodeProperties>(() => {
|
||||
return {
|
||||
name: '',
|
||||
displayName: '',
|
||||
default: '',
|
||||
placeholder: 'value',
|
||||
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
|
||||
};
|
||||
});
|
||||
|
||||
const hint = computed(() => {
|
||||
const { value } = assignment.value;
|
||||
if (typeof value !== 'string' || !value.startsWith('=')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
try {
|
||||
const resolvedValue = resolveParameter(value) as unknown;
|
||||
|
||||
if (isObject(resolvedValue)) {
|
||||
return JSON.stringify(resolvedValue);
|
||||
}
|
||||
if (typeof resolvedValue === 'boolean' || typeof resolvedValue === 'number') {
|
||||
return resolvedValue.toString();
|
||||
}
|
||||
|
||||
return resolvedValue as string;
|
||||
} catch (error) {
|
||||
return '';
|
||||
}
|
||||
});
|
||||
|
||||
const valueIsExpression = computed(() => {
|
||||
const { value } = assignment.value;
|
||||
|
||||
return typeof value === 'string' && isExpression(value);
|
||||
});
|
||||
|
||||
const onAssignmentNameChange = (update: IUpdateInformation): void => {
|
||||
assignment.value.name = update.value as string;
|
||||
};
|
||||
|
||||
const onAssignmentTypeChange = (update: string): void => {
|
||||
assignment.value.type = update;
|
||||
|
||||
if (update === 'boolean' && !valueIsExpression.value) {
|
||||
assignment.value.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onAssignmentValueChange = (update: IUpdateInformation): void => {
|
||||
assignment.value.value = update.value as string;
|
||||
};
|
||||
|
||||
const onRemove = (): void => {
|
||||
emit('remove');
|
||||
};
|
||||
|
||||
const onBlur = (): void => {
|
||||
emit('update:model-value', assignment.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
[$style.wrapper]: true,
|
||||
[$style.hasIssues]: issues.length > 0,
|
||||
[$style.hasHint]: !!hint,
|
||||
}"
|
||||
data-test-id="assignment"
|
||||
>
|
||||
<n8n-icon-button
|
||||
v-if="!isReadOnly"
|
||||
type="tertiary"
|
||||
text
|
||||
size="mini"
|
||||
icon="trash"
|
||||
data-test-id="assignment-remove"
|
||||
:class="$style.remove"
|
||||
@click="onRemove"
|
||||
></n8n-icon-button>
|
||||
|
||||
<div :class="$style.inputs">
|
||||
<InputTriple middle-width="100px">
|
||||
<template #left>
|
||||
<ParameterInputFull
|
||||
:key="nameParameter.type"
|
||||
display-options
|
||||
hide-label
|
||||
hide-hint
|
||||
:rows="3"
|
||||
:is-read-only="isReadOnly"
|
||||
:parameter="nameParameter"
|
||||
:value="assignment.name"
|
||||
:path="`${path}.name`"
|
||||
data-test-id="assignment-name"
|
||||
@update="onAssignmentNameChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
<template v-if="!hideType" #middle>
|
||||
<TypeSelect
|
||||
:class="$style.select"
|
||||
:model-value="assignment.type ?? 'string'"
|
||||
:is-read-only="isReadOnly"
|
||||
@update:model-value="onAssignmentTypeChange"
|
||||
>
|
||||
</TypeSelect>
|
||||
</template>
|
||||
<template #right="{ breakpoint }">
|
||||
<div :class="$style.value">
|
||||
<ParameterInputFull
|
||||
:key="valueParameter.type"
|
||||
display-options
|
||||
hide-label
|
||||
hide-issues
|
||||
hide-hint
|
||||
:rows="3"
|
||||
is-assignment
|
||||
:is-read-only="isReadOnly"
|
||||
:options-position="breakpoint === 'default' ? 'top' : 'bottom'"
|
||||
:parameter="valueParameter"
|
||||
:value="assignment.value"
|
||||
:path="`${path}.value`"
|
||||
data-test-id="assignment-value"
|
||||
@update="onAssignmentValueChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<ParameterInputHint :class="$style.hint" :hint="hint" single-line />
|
||||
</div>
|
||||
</template>
|
||||
</InputTriple>
|
||||
</div>
|
||||
|
||||
<div :class="$style.status">
|
||||
<ParameterIssues v-if="issues.length > 0" :issues="issues" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: var(--spacing-4xs);
|
||||
|
||||
&.hasIssues {
|
||||
--input-border-color: var(--color-danger);
|
||||
}
|
||||
|
||||
&.hasHint {
|
||||
padding-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.remove {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.inputs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
|
||||
> div {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.value {
|
||||
position: relative;
|
||||
|
||||
.hint {
|
||||
position: absolute;
|
||||
bottom: calc(var(--spacing-s) * -1);
|
||||
left: 0;
|
||||
right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: var(--spacing-l);
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in;
|
||||
}
|
||||
|
||||
.status {
|
||||
align-self: flex-start;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.statusIcon {
|
||||
padding-left: var(--spacing-4xs);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,270 @@
|
|||
<script setup lang="ts">
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import type {
|
||||
AssignmentCollectionValue,
|
||||
AssignmentValue,
|
||||
INode,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { computed, reactive, watch } from 'vue';
|
||||
import DropArea from '../DropArea/DropArea.vue';
|
||||
import ParameterOptions from '../ParameterOptions.vue';
|
||||
import Assignment from './Assignment.vue';
|
||||
import { inputDataToAssignments, nameFromExpression, typeFromExpression } from './utils';
|
||||
|
||||
interface Props {
|
||||
parameter: INodeProperties;
|
||||
value: AssignmentCollectionValue;
|
||||
path: string;
|
||||
node: INode | null;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), { isReadOnly: false });
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
event: 'valueChanged',
|
||||
value: { name: string; node: string; value: AssignmentCollectionValue },
|
||||
): void;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const state = reactive<{ paramValue: AssignmentCollectionValue }>({
|
||||
paramValue: {
|
||||
assignments: props.value.assignments ?? [],
|
||||
},
|
||||
});
|
||||
|
||||
const ndvStore = useNDVStore();
|
||||
const { callDebounced } = useDebounce();
|
||||
|
||||
const issues = computed(() => {
|
||||
if (!ndvStore.activeNode) return {};
|
||||
return ndvStore.activeNode?.issues?.parameters ?? {};
|
||||
});
|
||||
|
||||
const empty = computed(() => state.paramValue.assignments.length === 0);
|
||||
const activeDragField = computed(() => nameFromExpression(ndvStore.draggableData));
|
||||
const inputData = computed(() => ndvStore.ndvInputData?.[0]?.json);
|
||||
const actions = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: i18n.baseText('assignment.addAll'),
|
||||
value: 'addAll',
|
||||
disabled: !inputData.value,
|
||||
},
|
||||
{
|
||||
label: i18n.baseText('assignment.clearAll'),
|
||||
value: 'clearAll',
|
||||
disabled: state.paramValue.assignments.length === 0,
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
watch(state.paramValue, (value) => {
|
||||
void callDebounced(
|
||||
() => {
|
||||
emit('valueChanged', { name: props.path, value, node: props.node?.name as string });
|
||||
},
|
||||
{ debounceTime: 1000 },
|
||||
);
|
||||
});
|
||||
|
||||
function addAssignment(): void {
|
||||
state.paramValue.assignments.push({ id: uuid(), name: '', value: '', type: 'string' });
|
||||
}
|
||||
|
||||
function dropAssignment(expression: string): void {
|
||||
state.paramValue.assignments.push({
|
||||
id: uuid(),
|
||||
name: nameFromExpression(expression),
|
||||
value: `=${expression}`,
|
||||
type: typeFromExpression(expression),
|
||||
});
|
||||
}
|
||||
|
||||
function onAssignmentUpdate(index: number, value: AssignmentValue): void {
|
||||
state.paramValue.assignments[index] = value;
|
||||
}
|
||||
|
||||
function onAssignmentRemove(index: number): void {
|
||||
state.paramValue.assignments.splice(index, 1);
|
||||
}
|
||||
|
||||
function getIssues(index: number): string[] {
|
||||
return issues.value[`${props.parameter.name}.${index}`] ?? [];
|
||||
}
|
||||
|
||||
function optionSelected(action: 'clearAll' | 'addAll') {
|
||||
if (action === 'clearAll') {
|
||||
state.paramValue.assignments = [];
|
||||
} else {
|
||||
const newAssignments = inputDataToAssignments(inputData.value);
|
||||
state.paramValue.assignments = state.paramValue.assignments.concat(newAssignments);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{ [$style.assignmentCollection]: true, [$style.empty]: empty }"
|
||||
:data-test-id="`assignment-collection-${parameter.name}`"
|
||||
>
|
||||
<n8n-input-label
|
||||
:label="parameter.displayName"
|
||||
:show-expression-selector="false"
|
||||
size="small"
|
||||
underline
|
||||
color="text-dark"
|
||||
>
|
||||
<template #options>
|
||||
<ParameterOptions
|
||||
:parameter="parameter"
|
||||
:value="value"
|
||||
:custom-actions="actions"
|
||||
:is-read-only="isReadOnly"
|
||||
:show-expression-selector="false"
|
||||
@update:model-value="optionSelected"
|
||||
/>
|
||||
</template>
|
||||
</n8n-input-label>
|
||||
<div :class="$style.content">
|
||||
<div :class="$style.assignments">
|
||||
<div v-for="(assignment, index) of state.paramValue.assignments" :key="assignment.id">
|
||||
<Assignment
|
||||
:model-value="assignment"
|
||||
:index="index"
|
||||
:path="`${path}.${index}`"
|
||||
:issues="getIssues(index)"
|
||||
:class="$style.assignment"
|
||||
:is-read-only="isReadOnly"
|
||||
@update:model-value="(value) => onAssignmentUpdate(index, value)"
|
||||
@remove="() => onAssignmentRemove(index)"
|
||||
>
|
||||
</Assignment>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isReadOnly"
|
||||
:class="$style.dropAreaWrapper"
|
||||
data-test-id="assignment-collection-drop-area"
|
||||
@click="addAssignment"
|
||||
>
|
||||
<DropArea :sticky-offset="empty ? [-4, 32] : [92, 0]" @drop="dropAssignment">
|
||||
<template #default="{ active, droppable }">
|
||||
<div :class="{ [$style.active]: active, [$style.droppable]: droppable }">
|
||||
<div v-if="droppable" :class="$style.dropArea">
|
||||
<span>{{ i18n.baseText('assignment.dropField') }}</span>
|
||||
<span :class="$style.activeField">{{ activeDragField }}</span>
|
||||
</div>
|
||||
<div v-else :class="$style.dropArea">
|
||||
<span>{{ i18n.baseText('assignment.dragFields') }}</span>
|
||||
<span :class="$style.or">{{ i18n.baseText('assignment.or') }}</span>
|
||||
<span :class="$style.add">{{ i18n.baseText('assignment.add') }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DropArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.assignmentCollection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: var(--spacing-xs) 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.assignments {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.assignment {
|
||||
padding-left: var(--spacing-l);
|
||||
}
|
||||
|
||||
.dropAreaWrapper {
|
||||
cursor: pointer;
|
||||
|
||||
&:not(.empty .dropAreaWrapper) {
|
||||
padding-left: var(--spacing-l);
|
||||
}
|
||||
|
||||
&:hover .add {
|
||||
color: var(--color-primary-shade-1);
|
||||
}
|
||||
}
|
||||
|
||||
.dropArea {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-dark);
|
||||
gap: 1ch;
|
||||
min-height: 24px;
|
||||
|
||||
> span {
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.or {
|
||||
color: var(--color-text-light);
|
||||
font-size: var(--font-size-2xs);
|
||||
}
|
||||
|
||||
.add {
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.activeField {
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-ndv-droppable-parameter);
|
||||
}
|
||||
|
||||
.active {
|
||||
.activeField {
|
||||
color: var(--color-success);
|
||||
}
|
||||
}
|
||||
|
||||
.empty {
|
||||
.dropArea {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-3xs);
|
||||
min-height: 20vh;
|
||||
}
|
||||
|
||||
.droppable .dropArea {
|
||||
flex-direction: row;
|
||||
gap: 1ch;
|
||||
}
|
||||
|
||||
.content {
|
||||
gap: var(--spacing-s);
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,68 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import type { BaseTextKey } from '@/plugins/i18n';
|
||||
import { ASSIGNMENT_TYPES } from './constants';
|
||||
import { computed } from 'vue';
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:model-value', type: string): void;
|
||||
}>();
|
||||
|
||||
const i18n = useI18n();
|
||||
|
||||
const types = ASSIGNMENT_TYPES;
|
||||
|
||||
const icon = computed(() => types.find((type) => type.type === props.modelValue)?.icon ?? 'cube');
|
||||
|
||||
const onTypeChange = (type: string): void => {
|
||||
emit('update:model-value', type);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-select
|
||||
data-test-id="assignment-type-select"
|
||||
size="small"
|
||||
:model-value="modelValue"
|
||||
:disabled="isReadOnly"
|
||||
@update:model-value="onTypeChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<n8n-icon :class="$style.icon" :icon="icon" color="text-light" size="small" />
|
||||
</template>
|
||||
<n8n-option
|
||||
v-for="option in types"
|
||||
:key="option.type"
|
||||
:value="option.type"
|
||||
:label="i18n.baseText(`type.${option.type}` as BaseTextKey)"
|
||||
:class="$style.option"
|
||||
>
|
||||
<n8n-icon
|
||||
:icon="option.icon"
|
||||
:color="modelValue === option.type ? 'primary' : 'text-light'"
|
||||
size="small"
|
||||
/>
|
||||
<span>{{ i18n.baseText(`type.${option.type}` as BaseTextKey) }}</span>
|
||||
</n8n-option>
|
||||
</n8n-select>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.icon {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.option {
|
||||
display: flex;
|
||||
gap: var(--spacing-2xs);
|
||||
align-items: center;
|
||||
font-size: var(--font-size-s);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,54 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import Assignment from '../Assignment.vue';
|
||||
|
||||
const DEFAULT_SETUP = {
|
||||
pinia: createTestingPinia(),
|
||||
props: {
|
||||
path: 'parameters.fields.0',
|
||||
modelValue: {
|
||||
name: '',
|
||||
type: 'string',
|
||||
value: '',
|
||||
},
|
||||
issues: [],
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(Assignment, DEFAULT_SETUP);
|
||||
|
||||
describe('Assignment.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('can edit name, type and value', async () => {
|
||||
const { getByTestId, baseElement, emitted } = renderComponent();
|
||||
|
||||
const nameField = getByTestId('assignment-name').querySelector('input') as HTMLInputElement;
|
||||
const valueField = getByTestId('assignment-value').querySelector('input') as HTMLInputElement;
|
||||
|
||||
expect(getByTestId('assignment')).toBeInTheDocument();
|
||||
expect(getByTestId('assignment-name')).toBeInTheDocument();
|
||||
expect(getByTestId('assignment-value')).toBeInTheDocument();
|
||||
expect(getByTestId('assignment-type-select')).toBeInTheDocument();
|
||||
|
||||
await userEvent.type(nameField, 'New name');
|
||||
await userEvent.type(valueField, 'New value');
|
||||
|
||||
await userEvent.click(baseElement.querySelectorAll('.option')[3]);
|
||||
|
||||
expect(emitted('update:model-value')[0]).toEqual([
|
||||
{ name: 'New name', type: 'array', value: 'New value' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('can remove itself', async () => {
|
||||
const { getByTestId, emitted } = renderComponent();
|
||||
|
||||
await userEvent.click(getByTestId('assignment-remove'));
|
||||
|
||||
expect(emitted('remove')).toEqual([[]]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,121 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent, within } from '@testing-library/vue';
|
||||
import * as workflowHelpers from '@/mixins/workflowHelpers';
|
||||
import AssignmentCollection from '../AssignmentCollection.vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
|
||||
const DEFAULT_SETUP = {
|
||||
pinia: createTestingPinia(),
|
||||
props: {
|
||||
path: 'parameters.fields',
|
||||
node: {
|
||||
parameters: {},
|
||||
id: 'f63efb2d-3cc5-4500-89f9-b39aab19baf5',
|
||||
name: 'Edit Fields',
|
||||
type: 'n8n-nodes-base.set',
|
||||
typeVersion: 3.3,
|
||||
position: [1120, 380],
|
||||
credentials: {},
|
||||
disabled: false,
|
||||
},
|
||||
parameter: { name: 'fields', displayName: 'Fields To Set' },
|
||||
value: {},
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(AssignmentCollection, DEFAULT_SETUP);
|
||||
|
||||
const getInput = (e: HTMLElement): HTMLInputElement => {
|
||||
return e.querySelector('input') as HTMLInputElement;
|
||||
};
|
||||
|
||||
const getAssignmentType = (assignment: HTMLElement): string => {
|
||||
return getInput(within(assignment).getByTestId('assignment-type-select')).value;
|
||||
};
|
||||
|
||||
async function dropAssignment({
|
||||
key,
|
||||
value,
|
||||
dropArea,
|
||||
}: {
|
||||
key: string;
|
||||
value: unknown;
|
||||
dropArea: HTMLElement;
|
||||
}): Promise<void> {
|
||||
useNDVStore().draggableStartDragging({
|
||||
type: 'mapping',
|
||||
data: `{{ $json.${key} }}`,
|
||||
dimensions: null,
|
||||
});
|
||||
|
||||
vitest.spyOn(workflowHelpers, 'resolveParameter').mockReturnValueOnce(value as never);
|
||||
|
||||
await userEvent.hover(dropArea);
|
||||
await fireEvent.mouseUp(dropArea);
|
||||
}
|
||||
|
||||
describe('AssignmentCollection.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders empty state properly', async () => {
|
||||
const { getByTestId, queryByTestId } = renderComponent();
|
||||
expect(getByTestId('assignment-collection-fields')).toBeInTheDocument();
|
||||
expect(getByTestId('assignment-collection-fields')).toHaveClass('empty');
|
||||
expect(getByTestId('assignment-collection-drop-area')).toHaveTextContent(
|
||||
'Drag input fields here',
|
||||
);
|
||||
expect(queryByTestId('assignment')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('can add and remove assignments', async () => {
|
||||
const { getByTestId, findAllByTestId } = renderComponent();
|
||||
|
||||
await userEvent.click(getByTestId('assignment-collection-drop-area'));
|
||||
await userEvent.click(getByTestId('assignment-collection-drop-area'));
|
||||
|
||||
let assignments = await findAllByTestId('assignment');
|
||||
|
||||
expect(assignments.length).toEqual(2);
|
||||
|
||||
await userEvent.type(getInput(within(assignments[1]).getByTestId('assignment-name')), 'second');
|
||||
await userEvent.type(
|
||||
getInput(within(assignments[1]).getByTestId('assignment-value')),
|
||||
'secondValue',
|
||||
);
|
||||
await userEvent.click(within(assignments[0]).getByTestId('assignment-remove'));
|
||||
|
||||
assignments = await findAllByTestId('assignment');
|
||||
expect(assignments.length).toEqual(1);
|
||||
expect(getInput(within(assignments[0]).getByTestId('assignment-value'))).toHaveValue(
|
||||
'secondValue',
|
||||
);
|
||||
});
|
||||
|
||||
it('can add assignments by drag and drop (and infer type)', async () => {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getByTestId, findAllByTestId } = renderComponent({ pinia });
|
||||
const dropArea = getByTestId('assignment-collection-drop-area');
|
||||
|
||||
await dropAssignment({ key: 'boolKey', value: true, dropArea });
|
||||
await dropAssignment({ key: 'stringKey', value: 'stringValue', dropArea });
|
||||
await dropAssignment({ key: 'numberKey', value: 25, dropArea });
|
||||
await dropAssignment({ key: 'objectKey', value: {}, dropArea });
|
||||
await dropAssignment({ key: 'arrayKey', value: [], dropArea });
|
||||
|
||||
let assignments = await findAllByTestId('assignment');
|
||||
|
||||
expect(assignments.length).toBe(5);
|
||||
expect(getAssignmentType(assignments[0])).toEqual('Boolean');
|
||||
expect(getAssignmentType(assignments[1])).toEqual('String');
|
||||
expect(getAssignmentType(assignments[2])).toEqual('Number');
|
||||
expect(getAssignmentType(assignments[3])).toEqual('Object');
|
||||
expect(getAssignmentType(assignments[4])).toEqual('Array');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,41 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import TypeSelect from '../TypeSelect.vue';
|
||||
|
||||
const DEFAULT_SETUP = {
|
||||
pinia: createTestingPinia(),
|
||||
props: {
|
||||
modelValue: 'boolean',
|
||||
},
|
||||
};
|
||||
|
||||
const renderComponent = createComponentRenderer(TypeSelect, DEFAULT_SETUP);
|
||||
|
||||
describe('TypeSelect.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders default state correctly and emit events', async () => {
|
||||
const { getByTestId, baseElement, emitted } = renderComponent();
|
||||
expect(getByTestId('assignment-type-select')).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(
|
||||
getByTestId('assignment-type-select').querySelector('.select-trigger') as HTMLElement,
|
||||
);
|
||||
|
||||
const options = baseElement.querySelectorAll('.option');
|
||||
expect(options.length).toEqual(5);
|
||||
|
||||
expect(options[0]).toHaveTextContent('String');
|
||||
expect(options[1]).toHaveTextContent('Number');
|
||||
expect(options[2]).toHaveTextContent('Boolean');
|
||||
expect(options[3]).toHaveTextContent('Array');
|
||||
expect(options[4]).toHaveTextContent('Object');
|
||||
|
||||
await userEvent.click(options[2]);
|
||||
|
||||
expect(emitted('update:model-value')).toEqual([['boolean']]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,7 @@
|
|||
export const ASSIGNMENT_TYPES = [
|
||||
{ type: 'string', icon: 'font' },
|
||||
{ type: 'number', icon: 'hashtag' },
|
||||
{ type: 'boolean', icon: 'check-square' },
|
||||
{ type: 'array', icon: 'list' },
|
||||
{ type: 'object', icon: 'cube' },
|
||||
];
|
|
@ -0,0 +1,61 @@
|
|||
import { isObject } from 'lodash-es';
|
||||
import type { AssignmentValue, IDataObject } from 'n8n-workflow';
|
||||
import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
export function nameFromExpression(expression: string): string {
|
||||
return expression.replace(/^{{\s*|\s*}}$/g, '').replace('$json.', '');
|
||||
}
|
||||
|
||||
export function inferAssignmentType(value: unknown): string {
|
||||
if (typeof value === 'boolean') return 'boolean';
|
||||
if (typeof value === 'number') return 'number';
|
||||
if (typeof value === 'string') return 'string';
|
||||
if (Array.isArray(value)) return 'array';
|
||||
if (isObject(value)) return 'object';
|
||||
return 'string';
|
||||
}
|
||||
|
||||
export function typeFromExpression(expression: string): string {
|
||||
try {
|
||||
const resolved = resolveParameter(`=${expression}`);
|
||||
return inferAssignmentType(resolved);
|
||||
} catch (error) {
|
||||
return 'string';
|
||||
}
|
||||
}
|
||||
|
||||
export function inputDataToAssignments(input: IDataObject): AssignmentValue[] {
|
||||
const assignments: AssignmentValue[] = [];
|
||||
|
||||
function processValue(value: IDataObject, path: Array<string | number> = []) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((element, index) => {
|
||||
processValue(element, [...path, index]);
|
||||
});
|
||||
} else if (isObject(value)) {
|
||||
for (const [key, objectValue] of Object.entries(value)) {
|
||||
processValue(objectValue as IDataObject, [...path, key]);
|
||||
}
|
||||
} else {
|
||||
const stringPath = path.reduce((fullPath: string, part) => {
|
||||
if (typeof part === 'number') {
|
||||
return `${fullPath}[${part}]`;
|
||||
}
|
||||
return `${fullPath}.${part}`;
|
||||
}, '$json');
|
||||
|
||||
const expression = `={{ ${stringPath} }}`;
|
||||
assignments.push({
|
||||
id: uuid(),
|
||||
name: stringPath.replace('$json.', ''),
|
||||
value: expression,
|
||||
type: inferAssignmentType(value),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
processValue(input);
|
||||
|
||||
return assignments;
|
||||
}
|
|
@ -117,7 +117,11 @@ export default defineComponent({
|
|||
|
||||
const data =
|
||||
this.targetDataKey && this.draggingEl ? this.draggingEl.dataset.value : this.data || '';
|
||||
this.ndvStore.draggableStartDragging({ type: this.type, data: data || '' });
|
||||
this.ndvStore.draggableStartDragging({
|
||||
type: this.type,
|
||||
data: data || '',
|
||||
dimensions: this.draggingEl?.getBoundingClientRect() ?? null,
|
||||
});
|
||||
|
||||
this.$emit('dragstart', this.draggingEl);
|
||||
document.body.style.cursor = 'grabbing';
|
||||
|
|
|
@ -10,6 +10,7 @@ import type { PropType } from 'vue';
|
|||
import { mapStores } from 'pinia';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import type { XYPosition } from '@/Interface';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
|
@ -26,21 +27,18 @@ export default defineComponent({
|
|||
type: Array as PropType<number[]>,
|
||||
default: () => [0, 0],
|
||||
},
|
||||
stickyOrigin: {
|
||||
type: String as PropType<'top-left' | 'center'>,
|
||||
default: 'top-left',
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
hovering: false,
|
||||
dimensions: null as DOMRect | null,
|
||||
id: uuid(),
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('mousemove', this.onMouseMove);
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('mousemove', this.onMouseMove);
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
computed: {
|
||||
...mapStores(useNDVStore),
|
||||
isDragging(): boolean {
|
||||
|
@ -49,12 +47,57 @@ export default defineComponent({
|
|||
draggableType(): string {
|
||||
return this.ndvStore.draggableType;
|
||||
},
|
||||
draggableDimensions(): DOMRect | null {
|
||||
return this.ndvStore.draggable.dimensions;
|
||||
},
|
||||
droppable(): boolean {
|
||||
return !this.disabled && this.isDragging && this.draggableType === this.type;
|
||||
},
|
||||
activeDrop(): boolean {
|
||||
return this.droppable && this.hovering;
|
||||
},
|
||||
stickyPosition(): XYPosition | null {
|
||||
if (this.disabled || !this.sticky || !this.hovering || !this.dimensions) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (this.stickyOrigin === 'center') {
|
||||
return [
|
||||
this.dimensions.left +
|
||||
this.stickyOffset[0] +
|
||||
this.dimensions.width / 2 -
|
||||
(this.draggableDimensions?.width ?? 0) / 2,
|
||||
this.dimensions.top +
|
||||
this.stickyOffset[1] +
|
||||
this.dimensions.height / 2 -
|
||||
(this.draggableDimensions?.height ?? 0) / 2,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
this.dimensions.left + this.stickyOffset[0],
|
||||
this.dimensions.top + this.stickyOffset[1],
|
||||
];
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeDrop(active) {
|
||||
if (active) {
|
||||
this.ndvStore.setDraggableTarget({ id: this.id, stickyPosition: this.stickyPosition });
|
||||
} else if (this.ndvStore.draggable.activeTarget?.id === this.id) {
|
||||
// Only clear active target if it is this one
|
||||
this.ndvStore.setDraggableTarget(null);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('mousemove', this.onMouseMove);
|
||||
window.addEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('mousemove', this.onMouseMove);
|
||||
window.removeEventListener('mouseup', this.onMouseUp);
|
||||
},
|
||||
methods: {
|
||||
onMouseMove(e: MouseEvent) {
|
||||
|
@ -63,17 +106,12 @@ export default defineComponent({
|
|||
if (targetRef && this.isDragging) {
|
||||
const dim = targetRef.getBoundingClientRect();
|
||||
|
||||
this.dimensions = dim;
|
||||
this.hovering =
|
||||
e.clientX >= dim.left &&
|
||||
e.clientX <= dim.right &&
|
||||
e.clientY >= dim.top &&
|
||||
e.clientY <= dim.bottom;
|
||||
|
||||
if (!this.disabled && this.sticky && this.hovering) {
|
||||
const [xOffset, yOffset] = this.stickyOffset;
|
||||
|
||||
this.ndvStore.setDraggableStickyPos([dim.left + xOffset, dim.top + yOffset]);
|
||||
}
|
||||
}
|
||||
},
|
||||
onMouseUp(e: MouseEvent) {
|
||||
|
@ -83,15 +121,5 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
activeDrop(active) {
|
||||
if (active) {
|
||||
this.ndvStore.setDraggableTargetId(this.id);
|
||||
} else if (this.ndvStore.draggable.activeTargetId === this.id) {
|
||||
// Only clear active target if it is this one
|
||||
this.ndvStore.setDraggableTargetId(null);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
53
packages/editor-ui/src/components/DropArea/DropArea.vue
Normal file
53
packages/editor-ui/src/components/DropArea/DropArea.vue
Normal file
|
@ -0,0 +1,53 @@
|
|||
<script setup lang="ts">
|
||||
import DraggableTarget from '@/components/DraggableTarget.vue';
|
||||
|
||||
const emit = defineEmits<{ (event: 'drop', value: string): void }>();
|
||||
|
||||
const onDrop = (value: string) => {
|
||||
emit('drop', value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<DraggableTarget type="mapping" @drop="onDrop">
|
||||
<template #default="{ droppable, activeDrop }">
|
||||
<div
|
||||
data-test-id="drop-area"
|
||||
:class="{ [$style.area]: true, [$style.active]: activeDrop, [$style.droppable]: droppable }"
|
||||
>
|
||||
<slot :active="activeDrop" :droppable="droppable"></slot>
|
||||
</div>
|
||||
</template>
|
||||
</DraggableTarget>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.area {
|
||||
border: dashed 1px var(--color-foreground-dark);
|
||||
border-radius: var(--border-radius-large);
|
||||
background: var(--color-background-light);
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
font-size: var(--font-size-s);
|
||||
transition: border-color 0.1s ease-in;
|
||||
box-shadow: inset 0 0 0px 1.5px var(--color-background-xlight);
|
||||
|
||||
&:not(.active):hover {
|
||||
border-color: var(--color-ndv-droppable-parameter);
|
||||
background: var(--color-ndv-droppable-parameter-background);
|
||||
}
|
||||
}
|
||||
|
||||
.droppable {
|
||||
border-color: var(--color-ndv-droppable-parameter);
|
||||
border-width: 1.5px;
|
||||
background: var(--color-ndv-droppable-parameter-background);
|
||||
}
|
||||
|
||||
.active {
|
||||
border-color: var(--color-success);
|
||||
background: var(--color-ndv-droppable-parameter-active-background);
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,40 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import { createPinia, setActivePinia } from 'pinia';
|
||||
import DropArea from '../DropArea.vue';
|
||||
|
||||
const renderComponent = createComponentRenderer(DropArea, {
|
||||
pinia: createTestingPinia(),
|
||||
});
|
||||
|
||||
async function fireDrop(dropArea: HTMLElement): Promise<void> {
|
||||
useNDVStore().draggableStartDragging({
|
||||
type: 'mapping',
|
||||
data: '{{ $json.something }}',
|
||||
dimensions: null,
|
||||
});
|
||||
|
||||
await userEvent.hover(dropArea);
|
||||
await fireEvent.mouseUp(dropArea);
|
||||
}
|
||||
|
||||
describe('DropArea.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders default state correctly and emits drop events', async () => {
|
||||
const pinia = createPinia();
|
||||
setActivePinia(pinia);
|
||||
|
||||
const { getByTestId, emitted } = renderComponent({ pinia });
|
||||
expect(getByTestId('drop-area')).toBeInTheDocument();
|
||||
|
||||
await fireDrop(getByTestId('drop-area'));
|
||||
|
||||
expect(emitted('drop')).toEqual([['{{ $json.something }}']]);
|
||||
});
|
||||
});
|
|
@ -4,16 +4,22 @@
|
|||
:class="$style['expression-parameter-input']"
|
||||
@keydown.tab="onBlur"
|
||||
>
|
||||
<div :class="[$style['all-sections'], { [$style['focused']]: isFocused }]">
|
||||
<div
|
||||
:class="[
|
||||
$style['all-sections'],
|
||||
{ [$style.focused]: isFocused, [$style.assignment]: isAssignment },
|
||||
]"
|
||||
>
|
||||
<div :class="[$style['prepend-section'], 'el-input-group__prepend']">
|
||||
<ExpressionFunctionIcon />
|
||||
<span v-if="isAssignment">=</span>
|
||||
<ExpressionFunctionIcon v-else />
|
||||
</div>
|
||||
<InlineExpressionEditorInput
|
||||
ref="inlineInput"
|
||||
:model-value="modelValue"
|
||||
:is-read-only="isReadOnly"
|
||||
:target-item="hoveringItem"
|
||||
:is-single-line="isSingleLine"
|
||||
:rows="rows"
|
||||
:additional-data="additionalExpressionData"
|
||||
:path="path"
|
||||
@focus="onFocus"
|
||||
|
@ -77,7 +83,11 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSingleLine: {
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
isAssignment: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -145,10 +155,9 @@ export default defineComponent({
|
|||
}
|
||||
},
|
||||
onChange({ value, segments }: { value: string; segments: Segment[] }) {
|
||||
if (this.isDragging) return;
|
||||
|
||||
this.segments = segments;
|
||||
|
||||
if (this.isDragging) return;
|
||||
if (value === '=' + this.modelValue) return; // prevent report on change of target item
|
||||
|
||||
this.$emit('update:modelValue', value);
|
||||
|
@ -167,8 +176,6 @@ export default defineComponent({
|
|||
|
||||
.all-sections {
|
||||
height: 30px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
display: inline-table;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -181,6 +188,13 @@ export default defineComponent({
|
|||
}
|
||||
}
|
||||
|
||||
.assignment {
|
||||
.prepend-section {
|
||||
vertical-align: top;
|
||||
padding-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.expression-editor-modal-opener {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
|
@ -192,6 +206,8 @@ export default defineComponent({
|
|||
var(--input-border-style, var(--border-style-base))
|
||||
var(--input-border-width, var(--border-width-base));
|
||||
cursor: pointer;
|
||||
border-radius: 0;
|
||||
border-top-left-radius: var(--border-radius-base);
|
||||
|
||||
&:hover {
|
||||
border: var(--input-border-color, var(--border-color-base))
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<script setup lang="ts">
|
||||
import type { IUpdateInformation } from '@/Interface';
|
||||
import InputTriple from '@/components/InputTriple/InputTriple.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||
import { useI18n } from '@/composables/useI18n';
|
||||
import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||
import { DateTime } from 'luxon';
|
||||
import {
|
||||
FilterError,
|
||||
executeFilterCondition,
|
||||
type FilterOptionsValue,
|
||||
validateFieldType,
|
||||
type FilterConditionValue,
|
||||
type FilterOperatorType,
|
||||
type FilterOptionsValue,
|
||||
type INodeProperties,
|
||||
type NodeParameterValue,
|
||||
type NodePropertyTypes,
|
||||
FilterError,
|
||||
validateFieldType,
|
||||
} from 'n8n-workflow';
|
||||
import { computed, ref } from 'vue';
|
||||
import OperatorSelect from './OperatorSelect.vue';
|
||||
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
|
||||
import type { FilterOperator } from './types';
|
||||
import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||
type ConditionResult =
|
||||
| { status: 'resolve_error' }
|
||||
| { status: 'validation_error'; error: string }
|
||||
|
@ -58,15 +58,24 @@ const operatorId = computed<FilterOperatorId>(() => {
|
|||
});
|
||||
const operator = computed(() => OPERATORS_BY_ID[operatorId.value] as FilterOperator);
|
||||
|
||||
const operatorTypeToNodePropType = (operatorType: FilterOperatorType): NodePropertyTypes => {
|
||||
const operatorTypeToNodeProperty = (
|
||||
operatorType: FilterOperatorType,
|
||||
): Pick<INodeProperties, 'type' | 'options'> => {
|
||||
switch (operatorType) {
|
||||
case 'boolean':
|
||||
return {
|
||||
type: 'options',
|
||||
options: [
|
||||
{ name: 'true', value: true },
|
||||
{ name: 'false', value: false },
|
||||
],
|
||||
};
|
||||
case 'array':
|
||||
case 'object':
|
||||
case 'boolean':
|
||||
case 'any':
|
||||
return 'string';
|
||||
return { type: 'string' };
|
||||
default:
|
||||
return operatorType;
|
||||
return { type: operatorType };
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -119,7 +128,7 @@ const leftParameter = computed<INodeProperties>(() => ({
|
|||
operator.value.type === 'dateTime'
|
||||
? now.value
|
||||
: i18n.baseText('filter.condition.placeholderLeft'),
|
||||
type: operatorTypeToNodePropType(operator.value.type),
|
||||
...operatorTypeToNodeProperty(operator.value.type),
|
||||
}));
|
||||
|
||||
const rightParameter = computed<INodeProperties>(() => ({
|
||||
|
@ -130,7 +139,7 @@ const rightParameter = computed<INodeProperties>(() => ({
|
|||
operator.value.type === 'dateTime'
|
||||
? now.value
|
||||
: i18n.baseText('filter.condition.placeholderRight'),
|
||||
type: operatorTypeToNodePropType(operator.value.rightType ?? operator.value.type),
|
||||
...operatorTypeToNodeProperty(operator.value.type),
|
||||
}));
|
||||
|
||||
const onLeftValueChange = (update: IUpdateInformation): void => {
|
||||
|
@ -144,9 +153,11 @@ const onRightValueChange = (update: IUpdateInformation): void => {
|
|||
const convertToType = (value: unknown, type: FilterOperatorType): unknown => {
|
||||
if (type === 'any') return value;
|
||||
|
||||
const fallback = type === 'boolean' ? false : value;
|
||||
|
||||
return (
|
||||
validateFieldType('filter', condition.value.leftValue, type, { parseStrings: true }).newValue ??
|
||||
value
|
||||
fallback
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -202,64 +213,53 @@ const onBlur = (): void => {
|
|||
:class="$style.remove"
|
||||
@click="onRemove"
|
||||
></n8n-icon-button>
|
||||
<n8n-resize-observer
|
||||
:class="$style.observer"
|
||||
:breakpoints="[
|
||||
{ bp: 'stacked', width: 340 },
|
||||
{ bp: 'medium', width: 520 },
|
||||
]"
|
||||
>
|
||||
<template #default="{ bp }">
|
||||
<div
|
||||
:class="{
|
||||
[$style.condition]: true,
|
||||
[$style.hideRightInput]: operator.singleValue,
|
||||
[$style.stacked]: bp === 'stacked',
|
||||
[$style.medium]: bp === 'medium',
|
||||
}"
|
||||
>
|
||||
<ParameterInputFull
|
||||
v-if="!fixedLeftValue"
|
||||
:key="leftParameter.type"
|
||||
display-options
|
||||
hide-label
|
||||
hide-hint
|
||||
is-single-line
|
||||
:parameter="leftParameter"
|
||||
:value="condition.leftValue"
|
||||
:path="`${path}.left`"
|
||||
:class="[$style.input, $style.inputLeft]"
|
||||
:is-read-only="readOnly"
|
||||
data-test-id="filter-condition-left"
|
||||
@update="onLeftValueChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
<OperatorSelect
|
||||
:class="$style.select"
|
||||
:selected="`${operator.type}:${operator.operation}`"
|
||||
:read-only="readOnly"
|
||||
@operatorChange="onOperatorChange"
|
||||
></OperatorSelect>
|
||||
<ParameterInputFull
|
||||
v-if="!operator.singleValue"
|
||||
:key="rightParameter.type"
|
||||
display-options
|
||||
hide-label
|
||||
hide-hint
|
||||
is-single-line
|
||||
:options-position="bp === 'default' ? 'top' : 'bottom'"
|
||||
:parameter="rightParameter"
|
||||
:value="condition.rightValue"
|
||||
:path="`${path}.right`"
|
||||
:class="[$style.input, $style.inputRight]"
|
||||
:is-read-only="readOnly"
|
||||
data-test-id="filter-condition-right"
|
||||
@update="onRightValueChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</div>
|
||||
<InputTriple>
|
||||
<template #left>
|
||||
<ParameterInputFull
|
||||
v-if="!fixedLeftValue"
|
||||
:key="leftParameter.type"
|
||||
display-options
|
||||
hide-label
|
||||
hide-hint
|
||||
hide-issues
|
||||
:rows="3"
|
||||
:is-read-only="readOnly"
|
||||
:parameter="leftParameter"
|
||||
:value="condition.leftValue"
|
||||
:path="`${path}.left`"
|
||||
:class="[$style.input, $style.inputLeft]"
|
||||
data-test-id="filter-condition-left"
|
||||
@update="onLeftValueChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
</n8n-resize-observer>
|
||||
<template #middle>
|
||||
<OperatorSelect
|
||||
:selected="`${operator.type}:${operator.operation}`"
|
||||
:read-only="readOnly"
|
||||
@operatorChange="onOperatorChange"
|
||||
></OperatorSelect>
|
||||
</template>
|
||||
<template #right="{ breakpoint }" v-if="!operator.singleValue">
|
||||
<ParameterInputFull
|
||||
:key="rightParameter.type"
|
||||
display-options
|
||||
hide-label
|
||||
hide-hint
|
||||
hide-issues
|
||||
:rows="3"
|
||||
:is-read-only="readOnly"
|
||||
:options-position="breakpoint === 'default' ? 'top' : 'bottom'"
|
||||
:parameter="rightParameter"
|
||||
:value="condition.rightValue"
|
||||
:path="`${path}.right`"
|
||||
:class="[$style.input, $style.inputRight]"
|
||||
data-test-id="filter-condition-right"
|
||||
@update="onRightValueChange"
|
||||
@blur="onBlur"
|
||||
/>
|
||||
</template>
|
||||
</InputTriple>
|
||||
|
||||
<div :class="$style.status">
|
||||
<ParameterIssues v-if="allIssues.length > 0" :issues="allIssues" />
|
||||
|
@ -305,16 +305,6 @@ const onBlur = (): void => {
|
|||
}
|
||||
}
|
||||
|
||||
.condition {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.observer {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.status {
|
||||
align-self: flex-start;
|
||||
padding-top: 28px;
|
||||
|
@ -324,39 +314,6 @@ const onBlur = (): void => {
|
|||
padding-left: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.select {
|
||||
flex-shrink: 0;
|
||||
flex-grow: 0;
|
||||
flex-basis: 160px;
|
||||
--input-border-radius: 0;
|
||||
--input-border-right-color: transparent;
|
||||
}
|
||||
|
||||
.input {
|
||||
flex-shrink: 0;
|
||||
flex-basis: 160px;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.inputLeft {
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-right-radius: 0;
|
||||
--input-border-right-color: transparent;
|
||||
}
|
||||
|
||||
.inputRight {
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.hideRightInput {
|
||||
.select {
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
--input-border-right-color: var(--input-border-color-base);
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
|
@ -364,91 +321,4 @@ const onBlur = (): void => {
|
|||
opacity: 0;
|
||||
transition: opacity 100ms ease-in;
|
||||
}
|
||||
|
||||
.medium {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.select {
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: 0;
|
||||
--input-border-bottom-color: transparent;
|
||||
--input-border-right-color: var(--input-border-color-base);
|
||||
}
|
||||
|
||||
.inputLeft {
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: 0;
|
||||
--input-border-right-color: transparent;
|
||||
--input-border-bottom-color: transparent;
|
||||
}
|
||||
|
||||
.inputRight {
|
||||
flex-basis: 340px;
|
||||
flex-shrink: 1;
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
&.hideRightInput {
|
||||
.select {
|
||||
--input-border-bottom-color: var(--input-border-color-base);
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-bottom-left-radius: 0;
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.inputLeft {
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: 0;
|
||||
--input-border-bottom-color: var(--input-border-color-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stacked {
|
||||
display: block;
|
||||
|
||||
.select {
|
||||
width: 100%;
|
||||
--input-border-right-color: var(--input-border-color-base);
|
||||
--input-border-bottom-color: transparent;
|
||||
--input-border-radius: 0;
|
||||
}
|
||||
|
||||
.inputLeft {
|
||||
--input-border-right-color: var(--input-border-color-base);
|
||||
--input-border-bottom-color: transparent;
|
||||
--input-border-top-left-radius: var(--border-radius-base);
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-left-radius: 0;
|
||||
--input-border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.inputRight {
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
&.hideRightInput {
|
||||
.select {
|
||||
--input-border-bottom-color: var(--input-border-color-base);
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.inputLeft {
|
||||
--input-border-top-left-radius: var(--border-radius-base);
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-left-radius: 0;
|
||||
--input-border-bottom-right-radius: 0;
|
||||
--input-border-bottom-color: transparent;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -138,6 +138,7 @@ function getIssues(index: number): string[] {
|
|||
:underline="true"
|
||||
:show-options="true"
|
||||
:show-expression-selector="false"
|
||||
size="small"
|
||||
color="text-dark"
|
||||
>
|
||||
</n8n-input-label>
|
||||
|
|
|
@ -247,37 +247,37 @@ export const DEFAULT_OPERATOR_VALUE: FilterConditionValue['operator'] =
|
|||
export const OPERATOR_GROUPS: FilterOperatorGroup[] = [
|
||||
{
|
||||
id: 'string',
|
||||
name: 'filter.operatorGroup.string',
|
||||
name: 'type.string',
|
||||
icon: 'font',
|
||||
children: OPERATORS.filter((operator) => operator.type === 'string'),
|
||||
},
|
||||
{
|
||||
id: 'number',
|
||||
name: 'filter.operatorGroup.number',
|
||||
name: 'type.number',
|
||||
icon: 'hashtag',
|
||||
children: OPERATORS.filter((operator) => operator.type === 'number'),
|
||||
},
|
||||
{
|
||||
id: 'dateTime',
|
||||
name: 'filter.operatorGroup.date',
|
||||
name: 'type.dateTime',
|
||||
icon: 'calendar',
|
||||
children: OPERATORS.filter((operator) => operator.type === 'dateTime'),
|
||||
},
|
||||
{
|
||||
id: 'boolean',
|
||||
name: 'filter.operatorGroup.boolean',
|
||||
name: 'type.boolean',
|
||||
icon: 'check-square',
|
||||
children: OPERATORS.filter((operator) => operator.type === 'boolean'),
|
||||
},
|
||||
{
|
||||
id: 'array',
|
||||
name: 'filter.operatorGroup.array',
|
||||
name: 'type.array',
|
||||
icon: 'list',
|
||||
children: OPERATORS.filter((operator) => operator.type === 'array'),
|
||||
},
|
||||
{
|
||||
id: 'object',
|
||||
name: 'filter.operatorGroup.object',
|
||||
name: 'type.object',
|
||||
icon: 'cube',
|
||||
children: OPERATORS.filter((operator) => operator.type === 'object'),
|
||||
},
|
||||
|
|
|
@ -185,7 +185,6 @@ export default defineComponent({
|
|||
multipleValues(): boolean {
|
||||
return !!this.parameter.typeOptions?.multipleValues;
|
||||
},
|
||||
|
||||
parameterOptions(): INodePropertyCollection[] {
|
||||
if (this.multipleValues && isINodePropertyCollectionList(this.parameter.options)) {
|
||||
return this.parameter.options;
|
||||
|
|
|
@ -35,9 +35,9 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSingleLine: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
|
@ -92,7 +92,7 @@ export default defineComponent({
|
|||
mounted() {
|
||||
const extensions = [
|
||||
n8nLang(),
|
||||
inputTheme({ isSingleLine: this.isSingleLine }),
|
||||
inputTheme({ rows: this.rows }),
|
||||
Prec.highest(
|
||||
keymap.of([
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
|
|
|
@ -15,11 +15,12 @@ const commonThemeProps = {
|
|||
},
|
||||
};
|
||||
|
||||
export const inputTheme = ({ isSingleLine } = { isSingleLine: false }) => {
|
||||
export const inputTheme = ({ rows } = { rows: 5 }) => {
|
||||
const maxHeight = Math.max(rows * 22 + 8);
|
||||
const theme = EditorView.theme({
|
||||
...commonThemeProps,
|
||||
'&': {
|
||||
maxHeight: isSingleLine ? '30px' : '112px',
|
||||
maxHeight: `${maxHeight}px`,
|
||||
minHeight: '30px',
|
||||
width: '100%',
|
||||
fontSize: 'var(--font-size-2xs)',
|
||||
|
|
163
packages/editor-ui/src/components/InputTriple/InputTriple.vue
Normal file
163
packages/editor-ui/src/components/InputTriple/InputTriple.vue
Normal file
|
@ -0,0 +1,163 @@
|
|||
<script setup lang="ts">
|
||||
type Props = {
|
||||
middleWidth?: string;
|
||||
};
|
||||
withDefaults(defineProps<Props>(), { middleWidth: '160px' });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<n8n-resize-observer
|
||||
:class="{ [$style.observer]: true }"
|
||||
:breakpoints="[
|
||||
{ bp: 'stacked', width: 400 },
|
||||
{ bp: 'medium', width: 680 },
|
||||
]"
|
||||
>
|
||||
<template #default="{ bp }">
|
||||
<div :class="$style.background"></div>
|
||||
<div
|
||||
:class="{
|
||||
[$style.triple]: true,
|
||||
[$style.stacked]: bp === 'stacked',
|
||||
[$style.medium]: bp === 'medium',
|
||||
[$style.default]: bp === 'default',
|
||||
[$style.noRightSlot]: !$slots.right,
|
||||
[$style.noMiddleSlot]: !$slots.middle,
|
||||
}"
|
||||
>
|
||||
<div v-if="$slots.left" :class="$style.item">
|
||||
<slot name="left" :breakpoint="bp"></slot>
|
||||
</div>
|
||||
<div
|
||||
v-if="$slots.middle"
|
||||
:class="[$style.item, $style.middle]"
|
||||
:style="{ flexBasis: middleWidth }"
|
||||
>
|
||||
<slot name="middle" :breakpoint="bp"></slot>
|
||||
</div>
|
||||
<div v-if="$slots.right" :class="$style.item">
|
||||
<slot name="right" :breakpoint="bp"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</n8n-resize-observer>
|
||||
</template>
|
||||
|
||||
<style lang="scss" module>
|
||||
.triple {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.observer {
|
||||
--parameter-input-options-height: 22px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
background-color: var(--color-background-input-triple);
|
||||
top: var(--parameter-input-options-height);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
border: 1px solid var(--border-color-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.item {
|
||||
flex-shrink: 0;
|
||||
flex-basis: 240px;
|
||||
flex-grow: 1;
|
||||
--input-border-radius: 0;
|
||||
}
|
||||
|
||||
.default .item:not(:first-child):not(:focus-within + .item) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.middle {
|
||||
flex-grow: 0;
|
||||
flex-basis: 160px;
|
||||
padding-top: var(--parameter-input-options-height);
|
||||
}
|
||||
|
||||
.item:first-of-type {
|
||||
--input-border-top-left-radius: var(--border-radius-base);
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.item:last-of-type {
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-bottom-left-radius: 0;
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
.medium:not(.noRightSlot) {
|
||||
flex-wrap: wrap;
|
||||
|
||||
.middle {
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: 0;
|
||||
|
||||
&:not(:focus-within + .item) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
.item:first-of-type {
|
||||
--input-border-top-left-radius: var(--border-radius-base);
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.item:last-of-type {
|
||||
flex-basis: 400px;
|
||||
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
|
||||
&:not(:focus-within ~ .item) {
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.stacked {
|
||||
display: block;
|
||||
|
||||
.middle {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.middle:not(.item:last-of-type) {
|
||||
width: 100%;
|
||||
--input-border-radius: 0;
|
||||
}
|
||||
|
||||
.item:first-of-type {
|
||||
--input-border-top-left-radius: var(--border-radius-base);
|
||||
--input-border-top-right-radius: var(--border-radius-base);
|
||||
--input-border-bottom-left-radius: 0;
|
||||
--input-border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.item:not(:first-of-type):not(:focus-within + .item) {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.item:last-of-type {
|
||||
--input-border-top-left-radius: 0;
|
||||
--input-border-top-right-radius: 0;
|
||||
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,38 @@
|
|||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import InputTriple from '../InputTriple.vue';
|
||||
|
||||
const renderComponent = createComponentRenderer(InputTriple);
|
||||
|
||||
describe('InputTriple.vue', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders layout correctly', async () => {
|
||||
const { container } = renderComponent({
|
||||
props: { middleWidth: '200px' },
|
||||
slots: {
|
||||
left: '<div>left</div>',
|
||||
middle: '<div>middle</div>',
|
||||
right: '<div>right</div>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector('.triple')).toBeInTheDocument();
|
||||
expect(container.querySelectorAll('.item')).toHaveLength(3);
|
||||
expect(container.querySelector('.middle')).toHaveStyle('flex-basis: 200px');
|
||||
});
|
||||
|
||||
it('does not render missing slots', async () => {
|
||||
const { container } = renderComponent({
|
||||
props: { middleWidth: '200px' },
|
||||
slots: {
|
||||
left: '<div>left</div>',
|
||||
middle: '<div>middle</div>',
|
||||
},
|
||||
});
|
||||
|
||||
expect(container.querySelector('.triple')).toBeInTheDocument();
|
||||
expect(container.querySelectorAll('.item')).toHaveLength(2);
|
||||
});
|
||||
});
|
|
@ -46,7 +46,8 @@
|
|||
:model-value="expressionDisplayValue"
|
||||
:title="displayTitle"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-single-line="isSingleLine"
|
||||
:rows="rows"
|
||||
:is-assignment="isAssignment"
|
||||
:path="path"
|
||||
:additional-expression-data="additionalExpressionData"
|
||||
:class="{ 'ph-no-capture': shouldRedactValue }"
|
||||
|
@ -549,7 +550,11 @@ export default defineComponent({
|
|||
isReadOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
isSingleLine: {
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
isAssignment: {
|
||||
type: Boolean,
|
||||
},
|
||||
parameter: {
|
||||
|
@ -1314,7 +1319,11 @@ export default defineComponent({
|
|||
(!this.modelValue || this.modelValue === '[Object: null]')
|
||||
) {
|
||||
this.valueChanged('={{ 0 }}');
|
||||
} else if (this.parameter.type === 'number' || this.parameter.type === 'boolean') {
|
||||
} else if (
|
||||
this.parameter.type === 'number' ||
|
||||
this.parameter.type === 'boolean' ||
|
||||
typeof this.modelValue !== 'string'
|
||||
) {
|
||||
this.valueChanged(`={{ ${this.modelValue} }}`);
|
||||
} else {
|
||||
this.valueChanged(`=${this.modelValue}`);
|
||||
|
@ -1345,7 +1354,6 @@ export default defineComponent({
|
|||
// Strip the '=' from the beginning
|
||||
newValue = this.modelValue ? this.modelValue.toString().substring(1) : null;
|
||||
}
|
||||
|
||||
this.valueChanged(newValue);
|
||||
}
|
||||
} else if (command === 'refreshOptions') {
|
||||
|
@ -1416,6 +1424,7 @@ export default defineComponent({
|
|||
|
||||
.droppable {
|
||||
--input-border-color: var(--color-ndv-droppable-parameter);
|
||||
--input-border-right-color: var(--color-ndv-droppable-parameter);
|
||||
--input-border-style: dashed;
|
||||
|
||||
textarea,
|
||||
|
@ -1427,6 +1436,7 @@ export default defineComponent({
|
|||
|
||||
.activeDrop {
|
||||
--input-border-color: var(--color-success);
|
||||
--input-border-right-color: var(--color-success);
|
||||
--input-background-color: var(--color-foreground-xlight);
|
||||
--input-border-style: solid;
|
||||
|
||||
|
|
|
@ -49,7 +49,8 @@
|
|||
:model-value="value"
|
||||
:path="path"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-single-line="isSingleLine"
|
||||
:is-assignment="isAssignment"
|
||||
:rows="rows"
|
||||
:droppable="droppable"
|
||||
:active-drop="activeDrop"
|
||||
:force-show-expression="forceShowExpression"
|
||||
|
@ -140,7 +141,11 @@ export default defineComponent({
|
|||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isSingleLine: {
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
isAssignment: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
|
@ -387,6 +392,7 @@ export default defineComponent({
|
|||
position: absolute;
|
||||
bottom: -22px;
|
||||
right: 0;
|
||||
z-index: 1;
|
||||
opacity: 0;
|
||||
transition: opacity 100ms ease-in;
|
||||
|
||||
|
|
|
@ -117,6 +117,15 @@
|
|||
:read-only="isReadOnly"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
<AssignmentCollection
|
||||
v-else-if="parameter.type === 'assignmentCollection'"
|
||||
:parameter="parameter"
|
||||
:value="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)"
|
||||
:path="getPath(parameter.name)"
|
||||
:node="node"
|
||||
:is-read-only="isReadOnly"
|
||||
@valueChanged="valueChanged"
|
||||
/>
|
||||
<div
|
||||
v-else-if="displayNodeParameter(parameter) && credentialsParameterIndex !== index"
|
||||
class="parameter-item"
|
||||
|
@ -170,7 +179,8 @@ import ImportParameter from '@/components/ImportParameter.vue';
|
|||
import MultipleParameter from '@/components/MultipleParameter.vue';
|
||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
||||
import Conditions from '@/components/FilterConditions/FilterConditions.vue';
|
||||
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
|
||||
import AssignmentCollection from '@/components/AssignmentCollection/AssignmentCollection.vue';
|
||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
@ -199,7 +209,8 @@ export default defineComponent({
|
|||
CollectionParameter,
|
||||
ImportParameter,
|
||||
ResourceMapper,
|
||||
FilterConditions: Conditions,
|
||||
FilterConditions,
|
||||
AssignmentCollection,
|
||||
},
|
||||
mixins: [workflowHelpers],
|
||||
props: {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
:model-value="modelValue"
|
||||
:path="path"
|
||||
:is-read-only="isReadOnly"
|
||||
:is-assignment="isAssignment"
|
||||
:droppable="droppable"
|
||||
:active-drop="activeDrop"
|
||||
:force-show-expression="forceShowExpression"
|
||||
|
@ -15,10 +16,10 @@
|
|||
:error-highlight="errorHighlight"
|
||||
:is-for-credential="isForCredential"
|
||||
:event-source="eventSource"
|
||||
:expression-evaluated="expressionValueComputed"
|
||||
:expression-evaluated="evaluatedExpressionValue"
|
||||
:additional-expression-data="resolvedAdditionalExpressionData"
|
||||
:label="label"
|
||||
:is-single-line="isSingleLine"
|
||||
:rows="rows"
|
||||
:data-test-id="`parameter-input-${parsedParameterName}`"
|
||||
:event-bus="eventBus"
|
||||
@focus="onFocus"
|
||||
|
@ -45,28 +46,29 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import type { PropType } from 'vue';
|
||||
import { defineComponent } from 'vue';
|
||||
|
||||
import type { INodeUi, IUpdateInformation, TargetItem } from '@/Interface';
|
||||
import ParameterInput from '@/components/ParameterInput.vue';
|
||||
import InputHint from '@/components/ParameterInputHint.vue';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { isValueExpression, parseResourceMapperFieldName } from '@/utils/nodeTypesUtils';
|
||||
import type {
|
||||
IDataObject,
|
||||
INodeProperties,
|
||||
INodePropertyMode,
|
||||
IParameterLabel,
|
||||
NodeParameterValue,
|
||||
NodeParameterValueType,
|
||||
Result,
|
||||
} from 'n8n-workflow';
|
||||
import { isResourceLocatorValue } from 'n8n-workflow';
|
||||
import type { INodeUi, IUpdateInformation, TargetItem } from '@/Interface';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
import { isValueExpression, parseResourceMapperFieldName } from '@/utils/nodeTypesUtils';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useEnvironmentsStore } from '@/stores/environments.ee.store';
|
||||
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
|
||||
|
||||
import { get } from 'lodash-es';
|
||||
import type { EventBus } from 'n8n-design-system/utils';
|
||||
import { createEventBus } from 'n8n-design-system/utils';
|
||||
|
||||
|
@ -85,7 +87,11 @@ export default defineComponent({
|
|||
isReadOnly: {
|
||||
type: Boolean,
|
||||
},
|
||||
isSingleLine: {
|
||||
rows: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
isAssignment: {
|
||||
type: Boolean,
|
||||
},
|
||||
parameter: {
|
||||
|
@ -183,15 +189,14 @@ export default defineComponent({
|
|||
isInputParentOfActiveNode(): boolean {
|
||||
return this.ndvStore.isInputParentOfActiveNode;
|
||||
},
|
||||
expressionValueComputed(): string | null {
|
||||
evaluatedExpression(): Result<unknown, unknown> {
|
||||
const value = isResourceLocatorValue(this.modelValue)
|
||||
? this.modelValue.value
|
||||
: this.modelValue;
|
||||
if (!this.activeNode || !this.isValueExpression || typeof value !== 'string') {
|
||||
return null;
|
||||
return { ok: false, error: '' };
|
||||
}
|
||||
|
||||
let computedValue: NodeParameterValue;
|
||||
try {
|
||||
let opts;
|
||||
if (this.ndvStore.isInputParentOfActiveNode) {
|
||||
|
@ -204,24 +209,39 @@ export default defineComponent({
|
|||
};
|
||||
}
|
||||
|
||||
computedValue = this.resolveExpression(value, undefined, opts);
|
||||
|
||||
if (computedValue === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof computedValue === 'string' && computedValue.length === 0) {
|
||||
return this.$locale.baseText('parameterInput.emptyString');
|
||||
}
|
||||
return { ok: true, result: this.resolveExpression(value, undefined, opts) };
|
||||
} catch (error) {
|
||||
computedValue = `[${this.$locale.baseText('parameterInput.error')}: ${error.message}]`;
|
||||
return { ok: false, error };
|
||||
}
|
||||
},
|
||||
evaluatedExpressionValue(): unknown {
|
||||
const evaluated = this.evaluatedExpression;
|
||||
return evaluated.ok ? evaluated.result : null;
|
||||
},
|
||||
evaluatedExpressionString(): string | null {
|
||||
const evaluated = this.evaluatedExpression;
|
||||
|
||||
if (!evaluated.ok) {
|
||||
return `[${this.$locale.baseText('parameterInput.error')}: ${get(
|
||||
evaluated.error,
|
||||
'message',
|
||||
)}]`;
|
||||
}
|
||||
|
||||
return typeof computedValue === 'string' ? computedValue : JSON.stringify(computedValue);
|
||||
if (evaluated.result === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (typeof evaluated.result === 'string' && evaluated.result.length === 0) {
|
||||
return this.$locale.baseText('parameterInput.emptyString');
|
||||
}
|
||||
return typeof evaluated.result === 'string'
|
||||
? evaluated.result
|
||||
: JSON.stringify(evaluated.result);
|
||||
},
|
||||
expressionOutput(): string | null {
|
||||
if (this.isValueExpression && this.expressionValueComputed) {
|
||||
return this.expressionValueComputed;
|
||||
if (this.isValueExpression && this.evaluatedExpressionString) {
|
||||
return this.evaluatedExpressionString;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -93,7 +93,7 @@
|
|||
ref="input"
|
||||
:model-value="expressionDisplayValue"
|
||||
:path="path"
|
||||
is-single-line
|
||||
:rows="1"
|
||||
@update:modelValue="onInputChange"
|
||||
@modalOpenerClick="$emit('modalOpenerClick')"
|
||||
/>
|
||||
|
|
|
@ -257,9 +257,7 @@ describe('FilterConditions.vue', () => {
|
|||
let conditions = await findAllByTestId('filter-condition');
|
||||
expect(conditions.length).toEqual(2);
|
||||
|
||||
const removeButton = conditions[0].querySelector('[data-test-id="filter-remove-condition"]');
|
||||
|
||||
await userEvent.click(removeButton as Element);
|
||||
await userEvent.click(within(conditions[0]).getByTestId('filter-remove-condition'));
|
||||
|
||||
conditions = await findAllByTestId('filter-condition');
|
||||
expect(conditions.length).toEqual(1);
|
||||
|
|
|
@ -2325,13 +2325,12 @@
|
|||
"executionUsage.button.upgrade": "Upgrade plan",
|
||||
"executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.",
|
||||
"executionUsage.ranOutOfExecutions.text": "You’re out of executions. Upgrade your plan to keep automating.",
|
||||
"filter.operatorGroup.basic": "Basic",
|
||||
"filter.operatorGroup.string": "String",
|
||||
"filter.operatorGroup.number": "Number",
|
||||
"filter.operatorGroup.date": "Date & Time",
|
||||
"filter.operatorGroup.boolean": "Boolean",
|
||||
"filter.operatorGroup.array": "Array",
|
||||
"filter.operatorGroup.object": "Object",
|
||||
"type.string": "String",
|
||||
"type.number": "Number",
|
||||
"type.dateTime": "Date & Time",
|
||||
"type.boolean": "Boolean",
|
||||
"type.array": "Array",
|
||||
"type.object": "Object",
|
||||
"filter.operator.equals": "is equal to",
|
||||
"filter.operator.notEquals": "is not equal to",
|
||||
"filter.operator.contains": "contains",
|
||||
|
@ -2371,6 +2370,12 @@
|
|||
"filter.condition.resolvedFalse": "This condition is false for the first input item",
|
||||
"filter.condition.placeholderLeft": "value1",
|
||||
"filter.condition.placeholderRight": "value2",
|
||||
"assignment.dragFields": "Drag input fields here",
|
||||
"assignment.dropField": "Drop here to add the field",
|
||||
"assignment.or": "or",
|
||||
"assignment.add": "Add Field",
|
||||
"assignment.addAll": "Add All",
|
||||
"assignment.clearAll": "Clear All",
|
||||
"templateSetup.title": "Set up '{name}' template",
|
||||
"templateSetup.instructions": "You need {0} account to setup this template",
|
||||
"templateSetup.skip": "Skip",
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { useStorage } from '@/composables/useStorage';
|
||||
import { LOCAL_STORAGE_MAPPING_IS_ONBOARDED, STORES } from '@/constants';
|
||||
import type {
|
||||
INodeUi,
|
||||
IRunDataDisplayMode,
|
||||
|
@ -8,7 +6,9 @@ import type {
|
|||
TargetItem,
|
||||
XYPosition,
|
||||
} from '@/Interface';
|
||||
import type { INodeIssues, IRunData } from 'n8n-workflow';
|
||||
import { useStorage } from '@/composables/useStorage';
|
||||
import { LOCAL_STORAGE_MAPPING_IS_ONBOARDED, STORES } from '@/constants';
|
||||
import type { INodeExecutionData, INodeIssues } from 'n8n-workflow';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import { defineStore } from 'pinia';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
@ -46,8 +46,8 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
activeTargetId: null,
|
||||
stickyPosition: null,
|
||||
dimensions: null,
|
||||
activeTarget: null,
|
||||
},
|
||||
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true',
|
||||
}),
|
||||
|
@ -56,7 +56,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
const workflowsStore = useWorkflowsStore();
|
||||
return workflowsStore.getNodeByName(this.activeNodeName || '');
|
||||
},
|
||||
ndvInputData(): IRunData[] {
|
||||
ndvInputData(): INodeExecutionData[] {
|
||||
const workflowsStore = useWorkflowsStore();
|
||||
const executionData = workflowsStore.getWorkflowExecution;
|
||||
const inputNodeName: string | undefined = this.input.nodeName;
|
||||
|
@ -94,7 +94,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
return this.draggable.data;
|
||||
},
|
||||
canDraggableDrop(): boolean {
|
||||
return this.draggable.activeTargetId !== null;
|
||||
return this.draggable.activeTarget !== null;
|
||||
},
|
||||
outputPanelEditMode(): NDVState['output']['editMode'] {
|
||||
return this.output.editMode;
|
||||
|
@ -106,7 +106,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
};
|
||||
},
|
||||
draggableStickyPos(): XYPosition | null {
|
||||
return this.draggable.stickyPosition;
|
||||
return this.draggable.activeTarget?.stickyPosition ?? null;
|
||||
},
|
||||
ndvInputNodeName(): string | undefined {
|
||||
return this.input.nodeName;
|
||||
|
@ -189,13 +189,21 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
setMappableNDVInputFocus(paramName: string): void {
|
||||
this.focusedMappableInput = paramName;
|
||||
},
|
||||
draggableStartDragging({ type, data }: { type: string; data: string }): void {
|
||||
draggableStartDragging({
|
||||
type,
|
||||
data,
|
||||
dimensions,
|
||||
}: {
|
||||
type: string;
|
||||
data: string;
|
||||
dimensions: DOMRect | null;
|
||||
}): void {
|
||||
this.draggable = {
|
||||
isDragging: true,
|
||||
type,
|
||||
data,
|
||||
activeTargetId: null,
|
||||
stickyPosition: null,
|
||||
dimensions,
|
||||
activeTarget: null,
|
||||
};
|
||||
},
|
||||
draggableStopDragging(): void {
|
||||
|
@ -203,15 +211,11 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
|||
isDragging: false,
|
||||
type: '',
|
||||
data: '',
|
||||
activeTargetId: null,
|
||||
stickyPosition: null,
|
||||
activeTarget: null,
|
||||
};
|
||||
},
|
||||
setDraggableStickyPos(position: XYPosition | null): void {
|
||||
this.draggable.stickyPosition = position;
|
||||
},
|
||||
setDraggableTargetId(id: string | null): void {
|
||||
this.draggable.activeTargetId = id;
|
||||
setDraggableTarget(target: NDVState['draggable']['activeTarget']): void {
|
||||
this.draggable.activeTarget = target;
|
||||
},
|
||||
setMappingTelemetry(telemetry: { [key: string]: string | number | boolean }): void {
|
||||
this.mappingTelemetry = { ...this.mappingTelemetry, ...telemetry };
|
||||
|
|
|
@ -21,6 +21,7 @@ export class FilterV2 implements INodeType {
|
|||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
outputNames: ['Kept', 'Discarded'],
|
||||
parameterPane: 'wide',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
|
|
|
@ -21,6 +21,7 @@ export class IfV2 implements INodeType {
|
|||
inputs: ['main'],
|
||||
outputs: ['main', 'main'],
|
||||
outputNames: ['true', 'false'],
|
||||
parameterPane: 'wide',
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
|
|
|
@ -12,7 +12,7 @@ export class Set extends VersionedNodeType {
|
|||
icon: 'fa:pen',
|
||||
group: ['input'],
|
||||
description: 'Add or edit fields on an input item and optionally remove other fields',
|
||||
defaultVersion: 3.2,
|
||||
defaultVersion: 3.3,
|
||||
};
|
||||
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
|
@ -21,6 +21,7 @@ export class Set extends VersionedNodeType {
|
|||
3: new SetV2(baseDescription),
|
||||
3.1: new SetV2(baseDescription),
|
||||
3.2: new SetV2(baseDescription),
|
||||
3.3: new SetV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
|
|
File diff suppressed because it is too large
Load diff
806
packages/nodes-base/nodes/Set/test/Set.v3_3.workflow.json
Normal file
806
packages/nodes-base/nodes/Set/test/Set.v3_3.workflow.json
Normal file
|
@ -0,0 +1,806 @@
|
|||
{
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": true,
|
||||
"instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "487e9636-56d5-4955-9193-17dee520bdd0",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [160, 4240]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {\n \"string\": {\n test2: \"hello\",\n test3: \" \",\n test4: \"\",\n test5: \"3\",\n test6: \"3,14\",\n test7: \"3.14\",\n test8: \"false\",\n test8: \"TRUE\",\n test9: \"false\",\n test10: \"1\",\n test11: '[\"apples\", \"oranges\"]',\n test12: '\"apples\", \"oranges\"',\n test13: '[1, 2]',\n test14: '{\"a\": 1, \"b\": { \"c\": 10, \"d\": \"test\"}}',\n test15: '{\"a\": 1}',\n test16: \"null\",\n test17: \"undefined\",\n test18: \"0\",\n },\n \"number\": {\n test1: 52472,\n test2: -1,\n test3: 0,\n test4: 1.334535,\n test5: null,\n test6: undefined,\n test7: 1,\n },\n \"boolean\": {\n // test1: 1,\n // test2: 0,\n test3: true,\n test4: false,\n },\n \"date\": {\n test1: $now,\n test2: \"2023-08-01T12:34:56Z\",\n test3: \"2016-05-25T09:24:15.123\",\n test4: \"Tue, 01 Nov 2016 13:23:12 +0630\",\n test5: \"2017-05-15 09:24:15\",\n test6: \"1542674993\",\n test7: 1542674993,\n },\n \"array\": {\n test13: [1,2,3,4],\n },\n \"object\": {\n obj: {\n objKey: 2,\n objArray: [1,2,3,4],\n objBool: true\n }\n },\n }\n];"
|
||||
},
|
||||
"id": "8afc51b7-f9ed-417a-a58b-2fe251ba4b93",
|
||||
"name": "Code1",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [420, 4240]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "numberToString1",
|
||||
"type": "string",
|
||||
"value": "={{ $json.number.test1 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "numberToString2",
|
||||
"type": "string",
|
||||
"value": "={{ $json.number.test2 }}"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "numberToString3",
|
||||
"type": "string",
|
||||
"value": "={{ $json.number.test4 }}"
|
||||
},
|
||||
{
|
||||
"id": "1",
|
||||
"name": "boolToString1",
|
||||
"type": "string",
|
||||
"value": "={{ $json.boolean.test3 }}"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "boolToString2",
|
||||
"type": "string",
|
||||
"value": "={{ $json.boolean.test4 }}"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "arrayToString1",
|
||||
"type": "string",
|
||||
"value": "={{ $json.array.test13 }}"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"name": "objectToString1",
|
||||
"type": "string",
|
||||
"value": "={{ $json.object.obj }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "8ede737c-5648-4c8e-82b7-fa5193d8ff92",
|
||||
"name": "To String",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [780, 3860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToNumber1",
|
||||
"type": "number",
|
||||
"value": "={{ $json.string.test5 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToNumber2",
|
||||
"type": "number",
|
||||
"value": "={{ $json.string.test7 }}"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "boolToNumber1",
|
||||
"type": "number",
|
||||
"value": "={{ $json.boolean.test3 }}"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "boolToNumber2",
|
||||
"type": "number",
|
||||
"value": "={{ $json.boolean.test4 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "064976c4-b13d-4e2e-9a07-bab165b0efd7",
|
||||
"name": "To Number",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [780, 4060]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToBoolean1",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test8 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToBoolean3",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test9 }}"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "stringToBoolean4",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test10 }}"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "stringToBoolean5",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test18 }}"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "numberToBoolean1",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.number.test3 }}"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"name": "numberToBoolean2",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.number.test7 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "dfcdddde-cf89-44c5-94e7-f11e1f7e1a58",
|
||||
"name": "To Boolean",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [780, 4240]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToArray1",
|
||||
"type": "array",
|
||||
"value": "={{ $json.string.test11 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToArray2",
|
||||
"type": "array",
|
||||
"value": "={{ $json.string.test13 }}"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "arrayToArray1",
|
||||
"type": "array",
|
||||
"value": "={{ $json.array.test13 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "7ea149df-c231-40fc-a95c-c062860896e6",
|
||||
"name": "To Array",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [780, 4440]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToObject1",
|
||||
"type": "object",
|
||||
"value": "={{ $json.string.test14 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToObject2",
|
||||
"type": "object",
|
||||
"value": "={{ $json.string.test15 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "c775efa3-2508-466d-93b9-51d4c5ca9eca",
|
||||
"name": "To Object",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [780, 4620]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "### Strict type checking",
|
||||
"height": 1063.125,
|
||||
"width": 369.6875
|
||||
},
|
||||
"id": "d646183a-b6ea-48e5-99cc-a4be39959558",
|
||||
"name": "Sticky Note",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [700, 3800]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "### Loose type checking",
|
||||
"height": 1058.046875,
|
||||
"width": 310.703125
|
||||
},
|
||||
"id": "cb382bde-1cba-4019-a45a-b50fbeb2f8ac",
|
||||
"name": "Sticky Note1",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [1180, 3804.375]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToNumber1",
|
||||
"type": "number",
|
||||
"value": "={{ $json.string.test2 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToNumber2",
|
||||
"type": "number",
|
||||
"value": "={{ $json.string.test3 }}"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "stringToNumber3",
|
||||
"type": "number",
|
||||
"value": "={{ $json.string.test9 }}"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "arrayToNumber1",
|
||||
"type": "number",
|
||||
"value": "={{ $json.array.test13 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "d9c91c3d-de1e-430e-acb2-c832d1b41b21",
|
||||
"name": "To Number1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [1220, 3860]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToBoolean1",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test5 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToBoolean2",
|
||||
"type": "boolean",
|
||||
"value": "=3,14"
|
||||
},
|
||||
{
|
||||
"id": "3",
|
||||
"name": "stringToBoolean3",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test7 }}"
|
||||
},
|
||||
{
|
||||
"id": "4",
|
||||
"name": "stringToBoolean4",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test11 }}"
|
||||
},
|
||||
{
|
||||
"id": "5",
|
||||
"name": "stringToBoolean5",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test12 }}"
|
||||
},
|
||||
{
|
||||
"id": "6",
|
||||
"name": "stringToBoolean6",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.string.test17 }}"
|
||||
},
|
||||
{
|
||||
"id": "7",
|
||||
"name": "numberToBoolean1",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.number.test1 }}"
|
||||
},
|
||||
{
|
||||
"id": "8",
|
||||
"name": "numberToBoolean2",
|
||||
"type": "boolean",
|
||||
"value": "={{ $json.number.test4 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "2a62319a-9850-4aed-ba83-f7e1fae233d6",
|
||||
"name": "To Boolean1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [1220, 4060]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToArray1",
|
||||
"type": "array",
|
||||
"value": "={{ $json.string.test2 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToArray2",
|
||||
"type": "array",
|
||||
"value": "={{ $json.string.test5 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "72275d21-9a12-4030-98b9-4a8ef37e0158",
|
||||
"name": "To Array1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [1220, 4240]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "1",
|
||||
"name": "stringToObject1",
|
||||
"type": "object",
|
||||
"value": "={{ $json.string.test14 }}"
|
||||
},
|
||||
{
|
||||
"id": "2",
|
||||
"name": "stringToObject2",
|
||||
"type": "object",
|
||||
"value": "={{ $json.string.test15 }}"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {
|
||||
"ignoreConversionErrors": true
|
||||
}
|
||||
},
|
||||
"id": "c6b117f1-44b9-41ef-ad26-b8c77e37ed9e",
|
||||
"name": "To Object1",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [1220, 4440]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "8e3ce059-9c1a-4490-b5a8-6cfb22e43632",
|
||||
"name": "No Operation, do nothing",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [720, 3460]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "e581a88a-900e-4d9a-9dcd-6a4b6e30c39a",
|
||||
"name": "No Operation, do nothing1",
|
||||
"type": "n8n-nodes-base.noOp",
|
||||
"typeVersion": 1,
|
||||
"position": [980, 3460]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"content": "### Keep Input Fields",
|
||||
"height": 752.046875,
|
||||
"width": 310.703125
|
||||
},
|
||||
"id": "e03d8297-9eed-4679-9a8a-fc3e004ec0c7",
|
||||
"name": "Sticky Note2",
|
||||
"type": "n8n-nodes-base.stickyNote",
|
||||
"typeVersion": 1,
|
||||
"position": [700, 4940]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "c42efef4-240d-42b8-baf7-f9edc10a25ba",
|
||||
"name": "test",
|
||||
"value": "foo",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"options": {}
|
||||
},
|
||||
"id": "cf0799c0-cf07-43f9-89ce-0d218f13f23f",
|
||||
"name": "Keep All",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [800, 5000]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "ae431a73-282b-4547-b82a-f491ca244ad2",
|
||||
"name": "test",
|
||||
"value": "foo",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"include": "selected",
|
||||
"includeFields": "object, age",
|
||||
"options": {}
|
||||
},
|
||||
"id": "90575fc8-502c-4a22-ba27-bcf13a73e71a",
|
||||
"name": "Keep Selected",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [800, 5160]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "a1d28431-fd8e-4360-baec-f390515d57fd",
|
||||
"name": "test",
|
||||
"value": "foo",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"includeOtherFields": true,
|
||||
"include": "except",
|
||||
"excludeFields": "age",
|
||||
"options": {}
|
||||
},
|
||||
"id": "731be3d3-b08f-4e13-9f67-badae737d581",
|
||||
"name": "Keep All Except",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [800, 5320]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "23c2d8eb-7326-43ae-80b0-e651611f5f43",
|
||||
"name": "test",
|
||||
"value": "foo",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "f310db17-beb9-436c-99ec-e0fa1deea94d",
|
||||
"name": "Keep None",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [800, 5500]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [{name: 'John Doe', age: 32, object: {nested: true}}, {name: 'Jane Doe', age: 35, object: {nested: true}}]"
|
||||
},
|
||||
"id": "7f9a81c4-5207-493f-bf48-9e52e4c350c6",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"position": [480, 5240]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "To String",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Number",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Boolean",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Array",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Object",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "No Operation, do nothing",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"No Operation, do nothing": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "No Operation, do nothing1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"No Operation, do nothing1": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "To Number1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Boolean1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Array1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "To Object1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Keep All",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Keep Selected",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Keep All Except",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Keep None",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {
|
||||
"To String": [
|
||||
{
|
||||
"json": {
|
||||
"numberToString1": "52472",
|
||||
"numberToString2": "-1",
|
||||
"numberToString3": "1.334535",
|
||||
"boolToString1": "true",
|
||||
"boolToString2": "false",
|
||||
"arrayToString1": "[1,2,3,4]",
|
||||
"objectToString1": "{\"objKey\":2,\"objArray\":[1,2,3,4],\"objBool\":true}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Number": [
|
||||
{
|
||||
"json": {
|
||||
"stringToNumber1": 3,
|
||||
"stringToNumber2": 3.14,
|
||||
"boolToNumber1": 1,
|
||||
"boolToNumber2": 0
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Boolean": [
|
||||
{
|
||||
"json": {
|
||||
"stringToBoolean1": true,
|
||||
"stringToBoolean3": false,
|
||||
"stringToBoolean4": true,
|
||||
"stringToBoolean5": false,
|
||||
"numberToBoolean1": false,
|
||||
"numberToBoolean2": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Array": [
|
||||
{
|
||||
"json": {
|
||||
"stringToArray1": ["apples", "oranges"],
|
||||
"stringToArray2": [1, 2],
|
||||
"arrayToArray1": [1, 2, 3, 4]
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Object": [
|
||||
{
|
||||
"json": {
|
||||
"stringToObject1": {
|
||||
"a": 1,
|
||||
"b": {
|
||||
"c": 10,
|
||||
"d": "test"
|
||||
}
|
||||
},
|
||||
"stringToObject2": {
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Number1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToNumber1": "hello",
|
||||
"stringToNumber2": 0,
|
||||
"stringToNumber3": "false",
|
||||
"arrayToNumber1": [1, 2, 3, 4]
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Boolean1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToBoolean1": "3",
|
||||
"stringToBoolean2": "3,14",
|
||||
"stringToBoolean3": "3.14",
|
||||
"stringToBoolean4": "[\"apples\", \"oranges\"]",
|
||||
"stringToBoolean5": "\"apples\", \"oranges\"",
|
||||
"stringToBoolean6": "undefined",
|
||||
"numberToBoolean1": 52472,
|
||||
"numberToBoolean2": 1.334535
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Array1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToArray1": "hello",
|
||||
"stringToArray2": "3"
|
||||
}
|
||||
}
|
||||
],
|
||||
"To Object1": [
|
||||
{
|
||||
"json": {
|
||||
"stringToObject1": {
|
||||
"a": 1,
|
||||
"b": {
|
||||
"c": 10,
|
||||
"d": "test"
|
||||
}
|
||||
},
|
||||
"stringToObject2": {
|
||||
"a": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Keep All": [
|
||||
{
|
||||
"json": {
|
||||
"name": "John Doe",
|
||||
"age": 32,
|
||||
"object": {
|
||||
"nested": true
|
||||
},
|
||||
"test": "foo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"name": "Jane Doe",
|
||||
"age": 35,
|
||||
"object": {
|
||||
"nested": true
|
||||
},
|
||||
"test": "foo"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Keep Selected": [
|
||||
{
|
||||
"json": {
|
||||
"object": {
|
||||
"nested": true
|
||||
},
|
||||
"age": 32,
|
||||
"test": "foo"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"object": {
|
||||
"nested": true
|
||||
},
|
||||
"age": 35,
|
||||
"test": "foo"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
|
@ -181,11 +181,7 @@ describe('test Set2, parseJsonParameter', () => {
|
|||
|
||||
describe('test Set2, validateEntry', () => {
|
||||
it('should convert number to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: 42 as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
const result = validateEntry('foo', 'string', 42 as unknown as string, node, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
|
@ -194,11 +190,7 @@ describe('test Set2, validateEntry', () => {
|
|||
});
|
||||
|
||||
it('should convert array to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: [1, 2, 3] as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
const result = validateEntry('foo', 'string', [1, 2, 3] as unknown as string, node, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
|
@ -207,11 +199,7 @@ describe('test Set2, validateEntry', () => {
|
|||
});
|
||||
|
||||
it('should convert object to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: { foo: 'bar' } as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
const result = validateEntry('foo', 'string', { foo: 'bar' } as unknown as string, node, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
|
@ -220,11 +208,7 @@ describe('test Set2, validateEntry', () => {
|
|||
});
|
||||
|
||||
it('should convert boolean to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: true as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
const result = validateEntry('foo', 'string', true as unknown as string, node, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
|
@ -233,11 +217,7 @@ describe('test Set2, validateEntry', () => {
|
|||
});
|
||||
|
||||
it('should convert undefined to string', () => {
|
||||
const result = validateEntry(
|
||||
{ name: 'foo', type: 'stringValue', stringValue: undefined as unknown as string },
|
||||
node,
|
||||
0,
|
||||
);
|
||||
const result = validateEntry('foo', 'string', undefined as unknown as string, node, 0);
|
||||
|
||||
expect(result).toEqual({
|
||||
name: 'foo',
|
||||
|
|
|
@ -21,7 +21,7 @@ const versionDescription: INodeTypeDescription = {
|
|||
name: 'set',
|
||||
icon: 'fa:pen',
|
||||
group: ['input'],
|
||||
version: [3, 3.1, 3.2],
|
||||
version: [3, 3.1, 3.2, 3.3],
|
||||
description: 'Modify, add, or remove item fields',
|
||||
subtitle: '={{$parameter["mode"]}}',
|
||||
defaults: {
|
||||
|
@ -44,7 +44,7 @@ const versionDescription: INodeTypeDescription = {
|
|||
action: 'Edit item fields one by one',
|
||||
},
|
||||
{
|
||||
name: 'JSON Output',
|
||||
name: 'JSON',
|
||||
value: 'raw',
|
||||
description: 'Customize item output with JSON',
|
||||
action: 'Customize item output with JSON',
|
||||
|
@ -96,6 +96,11 @@ const versionDescription: INodeTypeDescription = {
|
|||
type: 'options',
|
||||
description: 'How to select the fields you want to include in your output items',
|
||||
default: 'all',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [3, 3.1, 3.2],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'All Input Fields',
|
||||
|
@ -119,6 +124,49 @@ const versionDescription: INodeTypeDescription = {
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Include Other Input Fields',
|
||||
name: 'includeOtherFields',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
description:
|
||||
"Whether to pass to the output all the input fields (along with the fields set in 'Fields to Set')",
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'@version': [3, 3.1, 3.2],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Input Fields to Include',
|
||||
name: 'include',
|
||||
type: 'options',
|
||||
description: 'How to select the fields you want to include in your output items',
|
||||
default: 'all',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'@version': [3, 3.1, 3.2],
|
||||
'/includeOtherFields': [false],
|
||||
},
|
||||
},
|
||||
options: [
|
||||
{
|
||||
name: 'All',
|
||||
value: INCLUDE.ALL,
|
||||
description: 'Also include all unchanged fields from the input',
|
||||
},
|
||||
{
|
||||
name: 'Selected',
|
||||
value: INCLUDE.SELECTED,
|
||||
description: 'Also include the fields listed in the parameter “Fields to Include”',
|
||||
},
|
||||
{
|
||||
name: 'All Except',
|
||||
value: INCLUDE.EXCEPT,
|
||||
description: 'Exclude the fields listed in the parameter “Fields to Exclude”',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Fields to Include',
|
||||
name: 'includeFields',
|
||||
|
@ -232,11 +280,16 @@ export class SetV2 implements INodeType {
|
|||
}
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const include = this.getNodeParameter('include', i) as IncludeMods;
|
||||
const includeOtherFields = this.getNodeParameter('includeOtherFields', i, false) as boolean;
|
||||
const include = this.getNodeParameter('include', i, 'all') as IncludeMods;
|
||||
const options = this.getNodeParameter('options', i, {});
|
||||
const node = this.getNode();
|
||||
|
||||
options.include = include;
|
||||
if (node.typeVersion >= 3.3) {
|
||||
options.include = includeOtherFields ? include : 'none';
|
||||
} else {
|
||||
options.include = include;
|
||||
}
|
||||
|
||||
const newItem = await setNode[mode].execute.call(
|
||||
this,
|
||||
|
|
|
@ -17,6 +17,12 @@ export type SetField = {
|
|||
objectValue?: string | IDataObject;
|
||||
};
|
||||
|
||||
export type AssignmentSetField = {
|
||||
name: string;
|
||||
value: unknown;
|
||||
type: string;
|
||||
};
|
||||
|
||||
export const INCLUDE = {
|
||||
ALL: 'all',
|
||||
NONE: 'none',
|
||||
|
|
|
@ -4,21 +4,22 @@ import type {
|
|||
IExecuteFunctions,
|
||||
INode,
|
||||
INodeExecutionData,
|
||||
ValidationResult,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
deepCopy,
|
||||
ApplicationError,
|
||||
NodeOperationError,
|
||||
deepCopy,
|
||||
jsonParse,
|
||||
validateFieldType,
|
||||
ApplicationError,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import set from 'lodash/set';
|
||||
import get from 'lodash/get';
|
||||
import set from 'lodash/set';
|
||||
import unset from 'lodash/unset';
|
||||
|
||||
import { getResolvables, sanitazeDataPathKey } from '../../../../utils/utilities';
|
||||
import type { SetNodeOptions, SetField } from './interfaces';
|
||||
import type { SetNodeOptions } from './interfaces';
|
||||
import { INCLUDE } from './interfaces';
|
||||
|
||||
const configureFieldHelper = (dotNotation?: boolean) => {
|
||||
|
@ -163,46 +164,43 @@ export const parseJsonParameter = (
|
|||
};
|
||||
|
||||
export const validateEntry = (
|
||||
entry: SetField,
|
||||
name: string,
|
||||
type: FieldType,
|
||||
value: unknown,
|
||||
node: INode,
|
||||
itemIndex: number,
|
||||
ignoreErrors = false,
|
||||
nodeVersion?: number,
|
||||
) => {
|
||||
let entryValue = entry[entry.type];
|
||||
const name = entry.name;
|
||||
|
||||
if (nodeVersion && nodeVersion >= 3.2 && (entryValue === undefined || entryValue === null)) {
|
||||
if (nodeVersion && nodeVersion >= 3.2 && (value === undefined || value === null)) {
|
||||
return { name, value: null };
|
||||
}
|
||||
|
||||
const entryType = entry.type.replace('Value', '') as FieldType;
|
||||
|
||||
const description = `To fix the error try to change the type for the field "${name}" or activate the option “Ignore Type Conversion Errors” to apply a less strict type validation`;
|
||||
|
||||
if (entryType === 'string') {
|
||||
if (nodeVersion && nodeVersion > 3 && (entryValue === undefined || entryValue === null)) {
|
||||
if (type === 'string') {
|
||||
if (nodeVersion && nodeVersion > 3 && (value === undefined || value === null)) {
|
||||
if (ignoreErrors) {
|
||||
return { name, value: null };
|
||||
} else {
|
||||
throw new NodeOperationError(
|
||||
node,
|
||||
`'${name}' expects a ${entryType} but we got '${String(entryValue)}' [item ${itemIndex}]`,
|
||||
`'${name}' expects a ${type} but we got '${String(value)}' [item ${itemIndex}]`,
|
||||
{ description },
|
||||
);
|
||||
}
|
||||
} else if (typeof entryValue === 'object') {
|
||||
entryValue = JSON.stringify(entryValue);
|
||||
} else if (typeof value === 'object') {
|
||||
value = JSON.stringify(value);
|
||||
} else {
|
||||
entryValue = String(entryValue);
|
||||
value = String(value);
|
||||
}
|
||||
}
|
||||
|
||||
const validationResult = validateFieldType(name, entryValue, entryType);
|
||||
const validationResult = validateFieldType(name, value, type);
|
||||
|
||||
if (!validationResult.valid) {
|
||||
if (ignoreErrors) {
|
||||
validationResult.newValue = entry[entry.type];
|
||||
validationResult.newValue = value as ValidationResult['newValue'];
|
||||
} else {
|
||||
const message = `${validationResult.errorMessage} [item ${itemIndex}]`;
|
||||
throw new NodeOperationError(node, message, {
|
||||
|
@ -212,9 +210,10 @@ export const validateEntry = (
|
|||
}
|
||||
}
|
||||
|
||||
const value = validationResult.newValue === undefined ? null : validationResult.newValue;
|
||||
|
||||
return { name, value };
|
||||
return {
|
||||
name,
|
||||
value: validationResult.newValue === undefined ? null : validationResult.newValue,
|
||||
};
|
||||
};
|
||||
|
||||
export function resolveRawData(this: IExecuteFunctions, rawData: string, i: number) {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import type {
|
||||
AssignmentCollectionValue,
|
||||
FieldType,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INode,
|
||||
|
@ -23,6 +25,11 @@ const properties: INodeProperties[] = [
|
|||
placeholder: 'Add Field',
|
||||
type: 'fixedCollection',
|
||||
description: 'Edit existing fields or add new ones to modify the output data',
|
||||
displayOptions: {
|
||||
show: {
|
||||
'@version': [3, 3.1, 3.2],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
|
@ -156,6 +163,17 @@ const properties: INodeProperties[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Fields to Set',
|
||||
name: 'assignments',
|
||||
type: 'assignmentCollection',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
'@version': [3, 3.1, 3.2],
|
||||
},
|
||||
},
|
||||
default: {},
|
||||
},
|
||||
];
|
||||
|
||||
const displayOptions = {
|
||||
|
@ -175,35 +193,60 @@ export async function execute(
|
|||
node: INode,
|
||||
) {
|
||||
try {
|
||||
const fields = this.getNodeParameter('fields.values', i, []) as SetField[];
|
||||
if (node.typeVersion < 3.3) {
|
||||
const fields = this.getNodeParameter('fields.values', i, []) as SetField[];
|
||||
|
||||
const newData: IDataObject = {};
|
||||
const newData: IDataObject = {};
|
||||
|
||||
for (const entry of fields) {
|
||||
if (
|
||||
entry.type === 'objectValue' &&
|
||||
rawFieldsData[entry.name] !== undefined &&
|
||||
entry.objectValue !== undefined &&
|
||||
entry.objectValue !== null
|
||||
) {
|
||||
entry.objectValue = parseJsonParameter(
|
||||
resolveRawData.call(this, rawFieldsData[entry.name] as string, i),
|
||||
for (const entry of fields) {
|
||||
if (
|
||||
entry.type === 'objectValue' &&
|
||||
rawFieldsData[entry.name] !== undefined &&
|
||||
entry.objectValue !== undefined &&
|
||||
entry.objectValue !== null
|
||||
) {
|
||||
entry.objectValue = parseJsonParameter(
|
||||
resolveRawData.call(this, rawFieldsData[entry.name] as string, i),
|
||||
node,
|
||||
i,
|
||||
entry.name,
|
||||
);
|
||||
}
|
||||
|
||||
const { name, value } = validateEntry(
|
||||
entry.name,
|
||||
entry.type.replace('Value', '') as FieldType,
|
||||
entry[entry.type],
|
||||
node,
|
||||
i,
|
||||
entry.name,
|
||||
options.ignoreConversionErrors,
|
||||
node.typeVersion,
|
||||
);
|
||||
newData[name] = value;
|
||||
}
|
||||
|
||||
const { name, value } = validateEntry(
|
||||
entry,
|
||||
node,
|
||||
i,
|
||||
options.ignoreConversionErrors,
|
||||
node.typeVersion,
|
||||
);
|
||||
newData[name] = value;
|
||||
return composeReturnItem.call(this, i, item, newData, options);
|
||||
}
|
||||
|
||||
const assignmentCollection = this.getNodeParameter(
|
||||
'assignments',
|
||||
i,
|
||||
) as AssignmentCollectionValue;
|
||||
const newData = Object.fromEntries(
|
||||
(assignmentCollection?.assignments ?? []).map((assignment) => {
|
||||
const { name, value } = validateEntry(
|
||||
assignment.name,
|
||||
assignment.type as FieldType,
|
||||
assignment.value,
|
||||
node,
|
||||
i,
|
||||
options.ignoreConversionErrors,
|
||||
node.typeVersion,
|
||||
);
|
||||
|
||||
return [name, value];
|
||||
}),
|
||||
);
|
||||
return composeReturnItem.call(this, i, item, newData, options);
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
|
|
|
@ -13,7 +13,7 @@ import type { SetNodeOptions } from './helpers/interfaces';
|
|||
|
||||
const properties: INodeProperties[] = [
|
||||
{
|
||||
displayName: 'JSON Output',
|
||||
displayName: 'JSON',
|
||||
name: 'jsonOutput',
|
||||
type: 'json',
|
||||
typeOptions: {
|
||||
|
|
|
@ -1083,6 +1083,7 @@ export type NodePropertyTypes =
|
|||
| 'curlImport'
|
||||
| 'resourceMapper'
|
||||
| 'filter'
|
||||
| 'assignmentCollection'
|
||||
| 'credentials';
|
||||
|
||||
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
||||
|
@ -1130,6 +1131,7 @@ export interface INodePropertyTypeOptions {
|
|||
expirable?: boolean; // Supported by: hidden (only in the credentials)
|
||||
resourceMapper?: ResourceMapperTypeOptions;
|
||||
filter?: FilterTypeOptions;
|
||||
assignment?: AssignmentTypeOptions;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
@ -1161,6 +1163,10 @@ export type FilterTypeOptions = Partial<{
|
|||
typeValidation: 'strict' | 'loose' | {}; // default = strict, `| {}` is a TypeScript trick to allow custom strings, but still give autocomplete
|
||||
}>;
|
||||
|
||||
export type AssignmentTypeOptions = Partial<{
|
||||
hideType?: boolean; // visible by default
|
||||
}>;
|
||||
|
||||
export type DisplayCondition =
|
||||
| { _cnd: { eq: NodeParameterValue } }
|
||||
| { _cnd: { not: NodeParameterValue } }
|
||||
|
@ -2287,6 +2293,17 @@ export type FilterValue = {
|
|||
combinator: FilterTypeCombinator;
|
||||
};
|
||||
|
||||
export type AssignmentCollectionValue = {
|
||||
assignments: AssignmentValue[];
|
||||
};
|
||||
|
||||
export type AssignmentValue = {
|
||||
id: string;
|
||||
name: string;
|
||||
value: NodeParameterValue;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
export interface ExecutionOptions {
|
||||
limit?: number;
|
||||
}
|
||||
|
@ -2463,3 +2480,5 @@ export type BannerName =
|
|||
| 'EMAIL_CONFIRMATION';
|
||||
|
||||
export type Functionality = 'regular' | 'configuration-node';
|
||||
|
||||
export type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
|
||||
|
|
|
@ -5,13 +5,12 @@ import type {
|
|||
FilterOptionsValue,
|
||||
FilterValue,
|
||||
INodeProperties,
|
||||
Result,
|
||||
ValidationResult,
|
||||
} from '../Interfaces';
|
||||
import { validateFieldType } from '../TypeValidation';
|
||||
import * as LoggerProxy from '../LoggerProxy';
|
||||
|
||||
type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
|
||||
|
||||
type FilterConditionMetadata = {
|
||||
index: number;
|
||||
unresolvedExpressions: boolean;
|
||||
|
|
Loading…
Reference in a new issue