refactor(editor): Move editor-ui and design-system to frontend dir (no-changelog) (#13564)

This commit is contained in:
Alex Grozav
2025-02-28 14:28:30 +02:00
committed by GitHub
parent 684353436d
commit f5743176e5
1635 changed files with 805 additions and 1079 deletions

View File

@@ -0,0 +1,65 @@
import { createComponentRenderer } from '@/__tests__/render';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import Assignment from './Assignment.vue';
import { defaultSettings } from '@/__tests__/defaults';
import { STORES } from '@/constants';
import { merge } from 'lodash-es';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
const DEFAULT_SETUP = {
pinia: createTestingPinia({
initialState: { [STORES.SETTINGS]: { settings: merge({}, defaultSettings) } },
}),
props: {
path: 'parameters.fields.0',
modelValue: {
name: '',
type: 'string',
value: '',
},
issues: [],
},
};
const renderComponent = createComponentRenderer(Assignment, DEFAULT_SETUP);
describe('Assignment.vue', () => {
beforeEach(() => {
createAppModals();
});
afterEach(() => {
cleanupAppModals();
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([[]]);
});
});

View File

@@ -0,0 +1,274 @@
<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 { useResolvedExpression } from '@/composables/useResolvedExpression';
import useEnvironmentsStore from '@/stores/environments.ee.store';
import { useNDVStore } from '@/stores/ndv.store';
import type { AssignmentValue, INodeProperties } from 'n8n-workflow';
import { computed, ref } from 'vue';
import TypeSelect from './TypeSelect.vue';
import { N8nIconButton } from '@n8n/design-system';
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<{
'update:model-value': [value: AssignmentValue];
remove: [];
}>();
const ndvStore = useNDVStore();
const environmentsStore = useEnvironmentsStore();
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: 'name',
displayName: 'Name',
default: '',
requiresDataPath: 'single',
placeholder: 'name',
type: 'string',
}));
const valueParameter = computed<INodeProperties>(() => {
return {
name: 'value',
displayName: 'Value',
default: '',
placeholder: 'value',
...assignmentTypeToNodeProperty(assignment.value.type ?? 'string'),
};
});
const value = computed(() => assignment.value.value);
const resolvedAdditionalExpressionData = computed(() => {
return { $vars: environmentsStore.variablesAsObject };
});
const { resolvedExpressionString, isExpression } = useResolvedExpression({
expression: value,
additionalData: resolvedAdditionalExpressionData,
});
const hint = computed(() => resolvedExpressionString.value);
const highlightHint = computed(() => Boolean(hint.value && ndvStore.getHoveringItem));
const onAssignmentNameChange = (update: IUpdateInformation): void => {
assignment.value.name = update.value as string;
};
const onAssignmentTypeChange = (update: string): void => {
assignment.value.type = update;
if (update === 'boolean' && !isExpression.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"
>
<N8nIconButton
v-if="!isReadOnly"
type="tertiary"
text
size="mini"
icon="grip-vertical"
:class="[$style.iconButton, $style.defaultTopPadding, 'drag-handle']"
></N8nIconButton>
<N8nIconButton
v-if="!isReadOnly"
type="tertiary"
text
size="mini"
icon="trash"
data-test-id="assignment-remove"
:class="[$style.iconButton, $style.extraTopPadding]"
@click="onRemove"
></N8nIconButton>
<div :class="$style.inputs">
<InputTriple middle-width="100px">
<template #left>
<ParameterInputFull
:key="nameParameter.type"
display-options
hide-label
hide-hint
: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
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
data-test-id="parameter-expression-preview-value"
:class="$style.hint"
:highlight="highlightHint"
: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 {
.iconButton {
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;
}
}
.iconButton {
position: absolute;
left: 0;
opacity: 0;
transition: opacity 100ms ease-in;
color: var(--icon-base-color);
}
.extraTopPadding {
top: calc(20px + var(--spacing-l));
}
.defaultTopPadding {
top: var(--spacing-l);
}
.status {
align-self: flex-start;
padding-top: 28px;
}
.statusIcon {
padding-left: var(--spacing-4xs);
}
</style>

View File

@@ -0,0 +1,154 @@
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 '@/composables/useWorkflowHelpers';
import AssignmentCollection from './AssignmentCollection.vue';
import { STORES } from '@/constants';
import { cleanupAppModals, createAppModals, SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
const DEFAULT_SETUP = {
pinia: createTestingPinia({
initialState: {
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
},
stubActions: false,
}),
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', () => {
beforeEach(() => {
createAppModals();
});
afterEach(() => {
vi.clearAllMocks();
cleanupAppModals();
});
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('does not break with saved assignments that have no ID (legacy)', async () => {
const { findAllByTestId } = renderComponent({
props: {
value: {
assignments: [
{ name: 'key1', value: 'value1', type: 'string' },
{ name: 'key2', value: 'value2', type: 'string' },
{ name: 'key3', value: 'value3', type: 'string' },
],
},
},
});
let assignments = await findAllByTestId('assignment');
expect(assignments.length).toEqual(3);
// Remove 2nd assignment
await userEvent.click(within(assignments[1]).getByTestId('assignment-remove'));
assignments = await findAllByTestId('assignment');
expect(assignments.length).toEqual(2);
expect(getInput(within(assignments[0]).getByTestId('assignment-value'))).toHaveValue('value1');
expect(getInput(within(assignments[1]).getByTestId('assignment-value'))).toHaveValue('value3');
});
it('can add assignments by drag and drop (and infer type)', async () => {
const { getByTestId, findAllByTestId } = renderComponent();
const dropArea = getByTestId('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 });
const 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');
});
});

View File

@@ -0,0 +1,299 @@
<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 { computed, reactive, watch } from 'vue';
import DropArea from '../DropArea/DropArea.vue';
import ParameterOptions from '../ParameterOptions.vue';
import Assignment from './Assignment.vue';
import { inputDataToAssignments, typeFromExpression } from './utils';
import { propertyNameFromExpression } from '@/utils/mappingUtils';
import Draggable from 'vuedraggable';
interface Props {
parameter: INodeProperties;
value: AssignmentCollectionValue;
path: string;
node: INode | null;
isReadOnly?: boolean;
}
const props = withDefaults(defineProps<Props>(), { isReadOnly: false });
const emit = defineEmits<{
valueChanged: [value: { name: string; node: string; value: AssignmentCollectionValue }];
}>();
const i18n = useI18n();
const state = reactive<{ paramValue: AssignmentCollectionValue }>({
paramValue: {
assignments:
props.value.assignments?.map((assignment) => {
if (!assignment.id) assignment.id = crypto.randomUUID();
return assignment;
}) ?? [],
},
});
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(() => propertyNameFromExpression(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: crypto.randomUUID(),
name: '',
value: '',
type: 'string',
});
}
function dropAssignment(expression: string): void {
state.paramValue.assignments.push({
id: crypto.randomUUID(),
name: propertyNameFromExpression(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: string) {
if (action === 'clearAll') {
state.paramValue.assignments = [];
} else if (action === 'addAll' && inputData.value) {
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">
<Draggable
v-model="state.paramValue.assignments"
item-key="id"
handle=".drag-handle"
:drag-class="$style.dragging"
:ghost-class="$style.ghost"
>
<template #item="{ index, element: assignment }">
<Assignment
:model-value="assignment"
:index="index"
:path="`${path}.assignments.${index}`"
:issues="getIssues(index)"
:class="$style.assignment"
:is-read-only="isReadOnly"
@update:model-value="(value) => onAssignmentUpdate(index, value)"
@remove="() => onAssignmentRemove(index)"
>
</Assignment>
</template>
</Draggable>
</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);
}
.ghost,
.dragging {
border-radius: var(--border-radius-base);
padding-right: var(--spacing-xs);
padding-bottom: var(--spacing-xs);
}
.ghost {
background-color: var(--color-background-base);
opacity: 0.5;
}
.dragging {
background-color: var(--color-background-xlight);
opacity: 0.7;
}
</style>

View File

@@ -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']]);
});
});

View File

@@ -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<{
'update:model-value': [type: string];
}>();
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>

View File

@@ -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' },
];

View File

@@ -0,0 +1,57 @@
import { isObject } from 'lodash-es';
import type { AssignmentValue, IDataObject } from 'n8n-workflow';
import { resolveParameter } from '@/composables/useWorkflowHelpers';
import { v4 as uuid } from 'uuid';
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;
}