mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
312 lines
7.2 KiB
Vue
312 lines
7.2 KiB
Vue
<script setup lang="ts">
|
|
import { useDebounce } from '@/composables/useDebounce';
|
|
import { useI18n } from '@n8n/i18n';
|
|
import { useNDVStore } from '@/stores/ndv.store';
|
|
import type {
|
|
AssignmentCollectionValue,
|
|
AssignmentValue,
|
|
FieldTypeMap,
|
|
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;
|
|
defaultType?: keyof FieldTypeMap;
|
|
disableType?: boolean;
|
|
node: INode | null;
|
|
isReadOnly?: boolean;
|
|
}
|
|
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
isReadOnly: false,
|
|
defaultType: undefined,
|
|
disableType: 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: props.defaultType ?? 'string',
|
|
});
|
|
}
|
|
|
|
function dropAssignment(expression: string): void {
|
|
state.paramValue.assignments.push({
|
|
id: crypto.randomUUID(),
|
|
name: propertyNameFromExpression(expression),
|
|
value: `=${expression}`,
|
|
type: props.defaultType ?? 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"
|
|
:disable-type="disableType"
|
|
@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 {
|
|
word-wrap: break-word;
|
|
overflow-wrap: break-word;
|
|
word-break: break-word;
|
|
white-space: normal;
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
|
|
.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>
|