n8n/packages/editor-ui/src/components/AssignmentCollection/AssignmentCollection.vue
Elias Meire 2799de491b
feat: Add assignment component with drag and drop to Set node (#8283)
Co-authored-by: Giulio Andreini <andreini@netseven.it>
2024-02-06 18:34:34 +01:00

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>