Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(editor): Overhaul input selector in NDV #9520

Merged
merged 12 commits into from
May 31, 2024
6 changes: 4 additions & 2 deletions cypress/pages/ndv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,9 @@ export class NDV extends BasePage {
httpRequestNotice: () => cy.getByTestId('node-parameters-http-notice'),
nthParam: (n: number) => cy.getByTestId('node-parameters').find('.parameter-item').eq(n),
inputRunSelector: () => this.getters.inputPanel().findChildByTestId('run-selector'),
inputLinkRun: () => this.getters.inputPanel().findChildByTestId('link-run'),
outputRunSelector: () => this.getters.outputPanel().findChildByTestId('run-selector'),
outputLinkRun: () => this.getters.outputPanel().findChildByTestId('link-run'),
outputHoveringItem: () => this.getters.outputPanel().findChildByTestId('hovering-item'),
inputHoveringItem: () => this.getters.inputPanel().findChildByTestId('hovering-item'),
outputBranches: () => this.getters.outputPanel().findChildByTestId('branches'),
Expand Down Expand Up @@ -228,10 +230,10 @@ export class NDV extends BasePage {
getVisibleSelect().find('.el-select-dropdown__item').contains(runName).click();
},
toggleOutputRunLinking: () => {
this.getters.outputRunSelector().find('button').click();
this.getters.outputLinkRun().click();
},
toggleInputRunLinking: () => {
this.getters.inputRunSelector().find('button').click();
this.getters.inputLinkRun().click();
},
switchOutputBranch: (name: string) => {
this.getters.outputBranches().get('span').contains(name).click();
Expand Down
193 changes: 193 additions & 0 deletions packages/editor-ui/src/components/InputNodeSelect.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<script lang="ts" setup>
import { useI18n } from '@/composables/useI18n';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { isPresent } from '@/utils/typesUtils';
import type { IConnectedNode, Workflow } from 'n8n-workflow';
import { computed } from 'vue';
import NodeIcon from './NodeIcon.vue';

type Props = {
nodes: IConnectedNode[];
workflow: Workflow;
modelValue: string | null;
};

const props = defineProps<Props>();

const emit = defineEmits<{
(event: 'update:model-value', value: string): void;
}>();

const i18n = useI18n();
const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore();

const selectedInputNode = computed(() => workflowsStore.getNodeByName(props.modelValue ?? ''));

const selectedInputNodeType = computed(() => {
const node = selectedInputNode.value;
if (!node) return null;

return nodeTypesStore.getNodeType(node.type, node.typeVersion);
});

const inputNodes = computed(() =>
props.nodes
.map((node) => {
const fullNode = workflowsStore.getNodeByName(node.name);
if (!fullNode) return null;

return {
node: fullNode,
type: nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion),
depth: node.depth,
};
})
.filter(isPresent),
);

const activeNode = computed(() => ndvStore.activeNode);

const activeNodeType = computed(() => {
const node = activeNode.value;
if (!node) return null;

return nodeTypesStore.getNodeType(node.type, node.typeVersion);
});

const isMultiInputNode = computed(() => {
const nodeType = activeNodeType.value;
return nodeType !== null && nodeType.inputs.length > 1;
});

function getMultipleNodesText(nodeName: string): string {
if (
!nodeName ||
!isMultiInputNode.value ||
!activeNode.value ||
!activeNodeType.value?.inputNames
)
return '';

const activeNodeConnections =
props.workflow.connectionsByDestinationNode[activeNode.value.name].main || [];
// Collect indexes of connected nodes
const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => {
if (node[0] && node[0].node === nodeName) return [...acc, index];
return acc;
}, []);

// Match connected input indexes to their names specified by active node
const connectedInputs = connectedInputIndexes.map(
(inputIndex) => activeNodeType.value?.inputNames?.[inputIndex],
);

if (connectedInputs.length === 0) return '';

return `(${connectedInputs.join(' & ')})`;
}

function title(nodeName: string) {
const truncated = nodeName.substring(0, 30);
if (truncated.length < nodeName.length) {
return `${truncated}...`;
}
return truncated;
}

function subtitle(nodeName: string, depth: number) {
const multipleNodesText = getMultipleNodesText(nodeName);
if (multipleNodesText) return multipleNodesText;
return i18n.baseText('ndv.input.nodeDistance', { adjustToNumber: depth });
}

function onInputNodeChange(value: string) {
emit('update:model-value', value);
}
</script>

<template>
<n8n-select
:model-value="modelValue"
:no-data-text="i18n.baseText('ndv.input.noNodesFound')"
:placeholder="i18n.baseText('ndv.input.parentNodes')"
:class="$style.select"
teleported
size="small"
filterable
data-test-id="ndv-input-select"
@update:model-value="onInputNodeChange"
>
<template #prefix>
<NodeIcon
:disabled="selectedInputNode?.disabled"
:node-type="selectedInputNodeType"
:size="14"
:shrink="false"
/>
</template>

<n8n-option
v-for="{ node, type, depth } of inputNodes"
:key="node.name"
:value="node.name"
:class="[$style.node, { [$style.disabled]: node.disabled }]"
:label="`${title(node.name)} ${getMultipleNodesText(node.name)}`"
data-test-id="ndv-input-option"
>
<NodeIcon
:disabled="node.disabled"
:node-type="type"
:size="14"
:shrink="false"
:class="$style.icon"
/>
<span :class="$style.title">
{{ title(node.name) }}
<span v-if="node.disabled">({{ i18n.baseText('node.disabled') }})</span>
</span>

<span :class="$style.subtitle">{{ subtitle(node.name, depth) }}</span>
</n8n-option>
</n8n-select>
</template>

<style lang="scss" module>
.select {
max-width: 224px;

:global(.el-input--suffix .el-input__inner) {
padding-left: calc(var(--spacing-l) + var(--spacing-4xs));
padding-right: var(--spacing-l);
}
}

.node {
--select-option-padding: 0 var(--spacing-s);
display: flex;
align-items: center;
font-size: var(--font-size-2xs);
gap: var(--spacing-4xs);
}

.icon {
padding-right: var(--spacing-4xs);
}

.title {
color: var(--color-text-dark);
font-weight: var(--font-weight-regular);
}

.disabled .title {
color: var(--color-text-light);
}

.subtitle {
color: var(--color-text-light);
font-weight: var(--font-weight-regular);
}
</style>
Loading
Loading