mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
271 lines
6.3 KiB
Vue
271 lines
6.3 KiB
Vue
<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>
|