mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): mapping expressions from input table (#3864)
* implement tree render
* update styles
* implement slots
* fix recursive tree rendering
* make not recursive
* Revert "make not recursive"
f064fc14f4
* enable dragging
* fix dragging name
* fix col bug
* update values and styles
* update style
* update colors
* update design
* add hover state
* add dragging behavior
* format file
* update pill text
* add depth field
* typo
* add avg height
* update event name
* update expr at distance
* add right margin always
* add space
* handle long values
* update types
* update messages
* update keys styling
* update spacing size
* fix hover bug
* update switch spacing
* fix wrap issue
* update spacing issues
* remove br
* update hoverable
* reduce event
* replace tree
* update prop name
* update tree story
* update tree
* refactor run data
* add unit tests
* add test for nodeclass
* remove number check
* bring back hook
* address review comments
* update margin
* update tests
* address max's feedback
* update tslint issues
* if empty, remove min width
* update spacing back
This commit is contained in:
parent
7d74ddab29
commit
ce076dca48
|
@ -0,0 +1,57 @@
|
|||
/* tslint:disable:variable-name */
|
||||
|
||||
import N8nTree from './Tree.vue';
|
||||
import {StoryFn} from "@storybook/vue";
|
||||
|
||||
export default {
|
||||
title: 'Atoms/Tree',
|
||||
component: N8nTree,
|
||||
};
|
||||
|
||||
export const Default: StoryFn = (args, {argTypes}) => ({
|
||||
props: Object.keys(argTypes),
|
||||
components: {
|
||||
N8nTree,
|
||||
},
|
||||
template: `<n8n-tree v-bind="$props">
|
||||
<template v-slot:label="{ label }">
|
||||
<span>{{ label }}</span>
|
||||
</template>
|
||||
<template v-slot:value="{ value }">
|
||||
<span>{{ value }}</span>
|
||||
</template>
|
||||
</n8n-tree>`,
|
||||
});
|
||||
|
||||
Default.args = {
|
||||
value: {
|
||||
objectKey: {
|
||||
nestedArrayKey: [
|
||||
'in progress',
|
||||
33958053,
|
||||
],
|
||||
stringKey: 'word',
|
||||
aLongKey: 'Lorem ipsum dolor sit consectetur adipiscing elit. Sed dignissim aliquam ipsum mattis pellentesque. Phasellus ut ligula fermentum orci elementum dignissim. Vivamus interdum risus eget nibh placerat ultrices. Vivamus orci arcu, iaculis in nulla non, blandit molestie magna. Praesent tristique feugiat odio non vehicula. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse fermentum purus diam, nec auctor elit consectetur nec. Vestibulum ultrices diam magna, in faucibus odio bibendum id. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut sollicitudin lacus neque.',
|
||||
objectKey: {
|
||||
myKey: 'what\'s for lunch',
|
||||
yourKey: 'prolle rewe wdyt',
|
||||
},
|
||||
id: 123,
|
||||
},
|
||||
hello: "world",
|
||||
test: {
|
||||
label: "A cool folder",
|
||||
children: [
|
||||
{
|
||||
label: "A cool sub-folder 1",
|
||||
children: [
|
||||
{ label: "A cool sub-sub-folder 1" },
|
||||
{ label: "A cool sub-sub-folder 2" },
|
||||
],
|
||||
},
|
||||
{ label: "This one is not that cool" },
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
82
packages/design-system/src/components/N8nTree/Tree.vue
Normal file
82
packages/design-system/src/components/N8nTree/Tree.vue
Normal file
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<div class="n8n-tree">
|
||||
<div v-for="(label, i) in Object.keys(value || {})" :key="i" :class="{[nodeClass]: !!nodeClass, [$style.indent]: depth > 0}">
|
||||
<div :class="$style.simple" v-if="isSimple(value[label])">
|
||||
<slot v-if="$scopedSlots.label" name="label" v-bind:label="label" v-bind:path="getPath(label)" />
|
||||
<span v-else>{{ label }}</span>
|
||||
<span>:</span>
|
||||
<slot v-if="$scopedSlots.value" name="value" v-bind:value="value[label]" />
|
||||
<span v-else>{{ value[label] }}</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<slot v-if="$scopedSlots.label" name="label" v-bind:label="label" v-bind:path="getPath(label)" />
|
||||
<span v-else>{{ label }}</span>
|
||||
<n8n-tree :path="getPath(label)" :depth="depth + 1" :value="value[label]" :nodeClass="nodeClass">
|
||||
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
|
||||
<slot :name="name" v-bind="data"></slot>
|
||||
</template>
|
||||
</n8n-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'n8n-tree',
|
||||
components: {
|
||||
},
|
||||
props: {
|
||||
value: {
|
||||
},
|
||||
path: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
depth: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
nodeClass: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isSimple(data: unkown): boolean {
|
||||
if (typeof data === 'object' && Object.keys(data).length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Array.isArray(data) && data.length === 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return typeof data !== 'object';
|
||||
},
|
||||
getPath(key: string): string[] {
|
||||
if (Array.isArray(this.value)) {
|
||||
return [...this.path, parseInt(key, 10)];
|
||||
}
|
||||
return [...this.path, key];
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
||||
$--spacing: var(--spacing-s);
|
||||
|
||||
.indent {
|
||||
margin-left: $--spacing;
|
||||
}
|
||||
|
||||
.simple {
|
||||
text-indent: calc($--spacing * -1);
|
||||
margin-left: $--spacing;
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,74 @@
|
|||
import { render } from '@testing-library/vue';
|
||||
import N8nTree from '../Tree.vue';
|
||||
|
||||
describe('components', () => {
|
||||
describe('N8nTree', () => {
|
||||
it('should render simple tree', () => {
|
||||
const wrapper = render(N8nTree, {
|
||||
props: {
|
||||
value: {
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tree', () => {
|
||||
const wrapper = render(N8nTree, {
|
||||
props: {
|
||||
value: {
|
||||
"hello": {
|
||||
"test": "world",
|
||||
},
|
||||
"options": [
|
||||
"yes",
|
||||
"no",
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render tree with slots', () => {
|
||||
const wrapper = render(N8nTree, {
|
||||
props: {
|
||||
value: {
|
||||
"hello": {
|
||||
"test": "world",
|
||||
},
|
||||
"options": [
|
||||
"yes",
|
||||
"no",
|
||||
],
|
||||
},
|
||||
},
|
||||
slots: {
|
||||
label: "<span>label</span>",
|
||||
value: "<span>value</span>",
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
|
||||
it('should render each tree with node class', () => {
|
||||
const wrapper = render(N8nTree, {
|
||||
props: {
|
||||
value: {
|
||||
"hello": {
|
||||
"test": "world",
|
||||
},
|
||||
"options": [
|
||||
"yes",
|
||||
"no",
|
||||
],
|
||||
},
|
||||
nodeClass: "nodeClass",
|
||||
},
|
||||
});
|
||||
expect(wrapper.html()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
exports[`components > N8nTree > should render each tree with node class 1`] = `
|
||||
"<div class=\\"n8n-tree\\">
|
||||
<div class=\\"nodeClass\\">
|
||||
<div><span>hello</span>
|
||||
<div class=\\"n8n-tree\\">
|
||||
<div class=\\"nodeClass _indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>test</span><span>:</span><span>world</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"nodeClass\\">
|
||||
<div><span>options</span>
|
||||
<div class=\\"n8n-tree\\">
|
||||
<div class=\\"nodeClass _indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>0</span><span>:</span><span>yes</span></div>
|
||||
</div>
|
||||
<div class=\\"nodeClass _indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>1</span><span>:</span><span>no</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`components > N8nTree > should render simple tree 1`] = `
|
||||
"<div class=\\"n8n-tree\\">
|
||||
<div class=\\"\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>hello</span><span>:</span><span>world</span></div>
|
||||
</div>
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`components > N8nTree > should render tree 1`] = `
|
||||
"<div class=\\"n8n-tree\\">
|
||||
<div class=\\"\\">
|
||||
<div><span>hello</span>
|
||||
<div class=\\"n8n-tree\\">
|
||||
<div class=\\"_indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>test</span><span>:</span><span>world</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"\\">
|
||||
<div><span>options</span>
|
||||
<div class=\\"n8n-tree\\">
|
||||
<div class=\\"_indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>0</span><span>:</span><span>yes</span></div>
|
||||
</div>
|
||||
<div class=\\"_indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>1</span><span>:</span><span>no</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"
|
||||
`;
|
||||
|
||||
exports[`components > N8nTree > should render tree with slots 1`] = `
|
||||
"<div class=\\"n8n-tree\\">
|
||||
<div class=\\"\\">
|
||||
<div><span>label</span>
|
||||
<div class=\\"n8n-tree\\">
|
||||
<div class=\\"_indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>label</span><span>:</span><span>value</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class=\\"\\">
|
||||
<div><span>label</span>
|
||||
<div class=\\"n8n-tree\\">
|
||||
<div class=\\"_indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>label</span><span>:</span><span>value</span></div>
|
||||
</div>
|
||||
<div class=\\"_indent_1y4uu_1\\">
|
||||
<div class=\\"_simple_1y4uu_5\\"><span>label</span><span>:</span><span>value</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>"
|
||||
`;
|
3
packages/design-system/src/components/N8nTree/index.ts
Normal file
3
packages/design-system/src/components/N8nTree/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
import Tree from './Tree.vue';
|
||||
|
||||
export default Tree;
|
|
@ -35,6 +35,7 @@ import N8nTabs from '../components/N8nTabs';
|
|||
import N8nTag from '../components/N8nTag';
|
||||
import N8nText from '../components/N8nText';
|
||||
import N8nTooltip from '../components/N8nTooltip';
|
||||
import N8nTree from '../components/N8nTree';
|
||||
import N8nUsersList from '../components/N8nUsersList';
|
||||
import N8nUserSelect from '../components/N8nUserSelect';
|
||||
|
||||
|
@ -76,6 +77,7 @@ export default {
|
|||
app.component('n8n-tag', N8nTag);
|
||||
app.component('n8n-text', N8nText);
|
||||
app.component('n8n-tooltip', N8nTooltip);
|
||||
app.component('n8n-tree', N8nTree);
|
||||
app.component('n8n-users-list', N8nUsersList);
|
||||
app.component('n8n-user-select', N8nUserSelect);
|
||||
},
|
||||
|
|
|
@ -31,7 +31,7 @@ import ColorCircles from './ColorCircles.vue';
|
|||
<Canvas>
|
||||
<Story name="secondary">
|
||||
{{
|
||||
template: `<color-circles :colors="['--color-secondary']" />`,
|
||||
template: `<color-circles :colors="['--color-secondary', '--color-secondary-tint-1', '--color-secondary-tint-2']" />`,
|
||||
components: {
|
||||
ColorCircles,
|
||||
},
|
||||
|
|
|
@ -384,8 +384,8 @@
|
|||
|
||||
--color-json-default: #5045A1;
|
||||
--color-json-null: var(--color-danger);
|
||||
--color-json-boolean: #1d8ce0;
|
||||
--color-json-number: #1d8ce0;
|
||||
--color-json-boolean: var(--color-success);
|
||||
--color-json-number: var(--color-success);
|
||||
--color-json-string: #5045A1;
|
||||
--color-json-key: var(--color-text-dark);
|
||||
--color-json-brackets: var(--color-text-dark);
|
||||
|
|
|
@ -221,6 +221,7 @@ export interface IRunDataUi {
|
|||
export interface ITableData {
|
||||
columns: string[];
|
||||
data: GenericValue[][];
|
||||
hasJson: {[key: string]: boolean};
|
||||
}
|
||||
|
||||
export interface IVariableItemSelected {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<template>
|
||||
<div
|
||||
<component :is="tag"
|
||||
:class="{[$style.dragging]: isDragging }"
|
||||
@mousedown="onDragStart"
|
||||
ref="wrapper"
|
||||
>
|
||||
<slot :isDragging="isDragging"></slot>
|
||||
|
||||
|
@ -12,10 +13,10 @@
|
|||
:style="draggableStyle"
|
||||
v-show="isDragging"
|
||||
>
|
||||
<slot name="preview" :canDrop="canDrop"></slot>
|
||||
<slot name="preview" :canDrop="canDrop" :el="draggingEl"></slot>
|
||||
</div>
|
||||
</Teleport>
|
||||
</div>
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -39,6 +40,13 @@ export default Vue.extend({
|
|||
data: {
|
||||
type: String,
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
default: 'div',
|
||||
},
|
||||
targetDataKey: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -47,6 +55,7 @@ export default Vue.extend({
|
|||
x: -100,
|
||||
y: -100,
|
||||
},
|
||||
draggingEl: null as null | HTMLElement,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
|
@ -69,12 +78,21 @@ export default Vue.extend({
|
|||
return;
|
||||
}
|
||||
|
||||
const target = e.target as HTMLElement;
|
||||
if (this.targetDataKey && target && target.dataset.target !== this.targetDataKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.draggingEl = target;
|
||||
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.isDragging = true;
|
||||
this.$store.commit('ui/draggableStartDragging', {type: this.type, data: this.data || ''});
|
||||
|
||||
this.$emit('dragstart');
|
||||
const data = this.targetDataKey ? target.dataset.value : (this.data || '');
|
||||
this.$store.commit('ui/draggableStartDragging', {type: this.type, data });
|
||||
|
||||
this.$emit('dragstart', this.draggingEl);
|
||||
document.body.style.cursor = 'grabbing';
|
||||
|
||||
window.addEventListener('mousemove', this.onDrag);
|
||||
|
@ -112,8 +130,9 @@ export default Vue.extend({
|
|||
window.removeEventListener('mouseup', this.onDragEnd);
|
||||
|
||||
setTimeout(() => {
|
||||
this.$emit('dragend');
|
||||
this.$emit('dragend', this.draggingEl);
|
||||
this.isDragging = false;
|
||||
this.draggingEl = null;
|
||||
this.$store.commit('ui/draggableStopDragging');
|
||||
}, 0);
|
||||
},
|
||||
|
|
|
@ -54,7 +54,7 @@ export default Vue.extend({
|
|||
onMouseMove(e: MouseEvent) {
|
||||
const target = this.$refs.target as HTMLElement;
|
||||
|
||||
if (target) {
|
||||
if (target && this.isDragging) {
|
||||
const dim = target.getBoundingClientRect();
|
||||
|
||||
this.hovering = e.clientX >= dim.left && e.clientX <= dim.right && e.clientY >= dim.top && e.clientY <= dim.bottom;
|
||||
|
|
|
@ -16,7 +16,9 @@
|
|||
paneType="input"
|
||||
@linkRun="onLinkRun"
|
||||
@unlinkRun="onUnlinkRun"
|
||||
@runChange="onRunIndexChange">
|
||||
@runChange="onRunIndexChange"
|
||||
@tableMounted="$emit('tableMounted', $event)"
|
||||
>
|
||||
<template v-slot:header>
|
||||
<div :class="$style.titleSection">
|
||||
<n8n-select v-if="parentNodes.length" :popper-append-to-body="true" size="small" :value="currentNodeName" @input="onSelect" :no-data-text="$locale.baseText('ndv.input.noNodesFound')" :placeholder="$locale.baseText('ndv.input.parentNodes')" filterable>
|
||||
|
|
|
@ -60,6 +60,7 @@
|
|||
@openSettings="openSettings"
|
||||
@select="onInputSelect"
|
||||
@execute="onNodeExecute"
|
||||
@tableMounted="onInputTableMounted"
|
||||
/>
|
||||
</template>
|
||||
<template #output>
|
||||
|
@ -73,6 +74,7 @@
|
|||
@unlinkRun="() => onUnlinkRun('output')"
|
||||
@runChange="onRunOutputIndexChange"
|
||||
@openSettings="openSettings"
|
||||
@tableMounted="onOutputTableMounted"
|
||||
/>
|
||||
</template>
|
||||
<template #main>
|
||||
|
@ -165,6 +167,8 @@ export default mixins(
|
|||
isDragging: false,
|
||||
mainPanelPosition: 0,
|
||||
pinDataDiscoveryTooltipVisible: false,
|
||||
avgInputRowHeight: 0,
|
||||
avgOutputRowHeight: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -341,6 +345,8 @@ export default mixins(
|
|||
this.isLinkingEnabled = true;
|
||||
this.selectedInput = undefined;
|
||||
this.triggerWaitingWarningEnabled = false;
|
||||
this.avgOutputRowHeight = 0;
|
||||
this.avgInputRowHeight = 0;
|
||||
|
||||
this.$store.commit('ui/setNDVSessionId');
|
||||
this.$externalHooks().run('dataDisplay.nodeTypeChanged', {
|
||||
|
@ -362,14 +368,16 @@ export default mixins(
|
|||
output_first_connector_runs: this.maxOutputRun,
|
||||
selected_view_inputs: this.isTriggerNode
|
||||
? 'trigger'
|
||||
: this.$store.getters['ui/inputPanelDispalyMode'],
|
||||
selected_view_outputs: this.$store.getters['ui/outputPanelDispalyMode'],
|
||||
: this.$store.getters['ui/inputPanelDisplayMode'],
|
||||
selected_view_outputs: this.$store.getters['ui/outputPanelDisplayMode'],
|
||||
input_connectors: this.parentNodes.length,
|
||||
output_connectors:
|
||||
outogingConnections && outogingConnections.main && outogingConnections.main.length,
|
||||
input_displayed_run_index: this.inputRun,
|
||||
output_displayed_run_index: this.outputRun,
|
||||
data_pinning_tooltip_presented: this.pinDataDiscoveryTooltipVisible,
|
||||
input_displayed_row_height_avg: this.avgInputRowHeight,
|
||||
output_displayed_row_height_avg: this.avgOutputRowHeight,
|
||||
});
|
||||
}
|
||||
}, 2000); // wait for RunData to mount and present pindata discovery tooltip
|
||||
|
@ -386,6 +394,12 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
onInputTableMounted(e: { avgRowHeight: number }) {
|
||||
this.avgInputRowHeight = e.avgRowHeight;
|
||||
},
|
||||
onOutputTableMounted(e: { avgRowHeight: number }) {
|
||||
this.avgOutputRowHeight = e.avgRowHeight;
|
||||
},
|
||||
onWorkflowActivate() {
|
||||
this.$store.commit('setActiveNode', null);
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
@runChange="onRunIndexChange"
|
||||
@linkRun="onLinkRun"
|
||||
@unlinkRun="onUnlinkRun"
|
||||
@tableMounted="$emit('tableMounted', $event)"
|
||||
ref="runData"
|
||||
>
|
||||
<template v-slot:header>
|
||||
|
|
|
@ -522,14 +522,6 @@ export default mixins(
|
|||
computedValue = `[${this.$locale.baseText('parameterInput.error')}}: ${error.message}]`;
|
||||
}
|
||||
|
||||
// Try to convert it into the corret type
|
||||
if (this.parameter.type === 'number') {
|
||||
computedValue = parseInt(computedValue as string, 10);
|
||||
if (isNaN(computedValue)) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return computedValue;
|
||||
},
|
||||
getStringInputType () {
|
||||
|
@ -1031,7 +1023,7 @@ export default mixins(
|
|||
}
|
||||
|
||||
.switch-input {
|
||||
margin: 2px 0;
|
||||
margin: var(--spacing-5xs) 0 var(--spacing-2xs) 0;
|
||||
}
|
||||
|
||||
.parameter-value-container {
|
||||
|
|
|
@ -224,7 +224,7 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table' && tableData && tableData.columns && tableData.columns.length === 0 && binaryData.length > 0" :class="$style.center">
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table' && binaryData.length > 0 && jsonData.length === 1 && Object.keys(jsonData[0] || {}).length === 0" :class="$style.center">
|
||||
<n8n-text>
|
||||
{{ $locale.baseText('runData.switchToBinary.info') }}
|
||||
<a @click="switchToBinary">
|
||||
|
@ -233,8 +233,8 @@
|
|||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table' && tableData" :class="$style.dataDisplay">
|
||||
<RunDataTable :node="node" :tableData="tableData" :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :showMappingHint="showMappingHint" :runIndex="runIndex" :totalRuns="maxRunIndex" />
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table'" :class="$style.dataDisplay">
|
||||
<RunDataTable :node="node" :inputData="inputData" :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :showMappingHint="showMappingHint" :runIndex="runIndex" :totalRuns="maxRunIndex" @mounted="$emit('tableMounted', $event)" />
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'json'" :class="$style.jsonDisplay">
|
||||
|
@ -649,9 +649,6 @@ export default mixins(
|
|||
jsonData (): IDataObject[] {
|
||||
return this.convertToJson(this.inputData);
|
||||
},
|
||||
tableData (): ITableData | undefined {
|
||||
return this.convertToTable(this.inputData);
|
||||
},
|
||||
binaryData (): IBinaryKeyData[] {
|
||||
if (!this.node) {
|
||||
return [];
|
||||
|
@ -1037,60 +1034,6 @@ export default mixins(
|
|||
|
||||
return returnData;
|
||||
},
|
||||
convertToTable (inputData: INodeExecutionData[]): ITableData | undefined {
|
||||
const tableData: GenericValue[][] = [];
|
||||
const tableColumns: string[] = [];
|
||||
let leftEntryColumns: string[], entryRows: GenericValue[];
|
||||
// Go over all entries
|
||||
let entry: IDataObject;
|
||||
inputData.forEach((data) => {
|
||||
if (!data.hasOwnProperty('json')) {
|
||||
return;
|
||||
}
|
||||
entry = data.json;
|
||||
|
||||
// Go over all keys of entry
|
||||
entryRows = [];
|
||||
leftEntryColumns = Object.keys(entry);
|
||||
|
||||
// Go over all the already existing column-keys
|
||||
tableColumns.forEach((key) => {
|
||||
if (entry.hasOwnProperty(key)) {
|
||||
// Entry does have key so add its value
|
||||
entryRows.push(entry[key]);
|
||||
// Remove key so that we know that it got added
|
||||
leftEntryColumns.splice(leftEntryColumns.indexOf(key), 1);
|
||||
} else {
|
||||
// Entry does not have key so add null
|
||||
entryRows.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Go over all the columns the entry has but did not exist yet
|
||||
leftEntryColumns.forEach((key) => {
|
||||
// Add the key for all runs in the future
|
||||
tableColumns.push(key);
|
||||
// Add the value
|
||||
entryRows.push(entry[key]);
|
||||
});
|
||||
|
||||
// Add the data of the entry
|
||||
tableData.push(entryRows);
|
||||
});
|
||||
|
||||
// Make sure that all entry-rows have the same length
|
||||
tableData.forEach((entryRows) => {
|
||||
if (tableColumns.length > entryRows.length) {
|
||||
// Has to less entries so add the missing ones
|
||||
entryRows.push.apply(entryRows, new Array(tableColumns.length - entryRows.length));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
columns: tableColumns,
|
||||
data: tableData,
|
||||
};
|
||||
},
|
||||
clearExecutionData () {
|
||||
this.$store.commit('setWorkflowExecutionData', null);
|
||||
this.updateNodesExecutionIssues();
|
||||
|
|
|
@ -3,23 +3,44 @@
|
|||
<table :class="$style.table" v-if="tableData.columns && tableData.columns.length === 0">
|
||||
<tr>
|
||||
<th :class="$style.emptyCell"></th>
|
||||
<th :class="$style.tableRightMargin"></th>
|
||||
</tr>
|
||||
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
||||
<td>
|
||||
<n8n-text>{{ $locale.baseText('runData.emptyItemHint') }}</n8n-text>
|
||||
</td>
|
||||
<td :class="$style.tableRightMargin"></td>
|
||||
</tr>
|
||||
</table>
|
||||
<table :class="$style.table" v-else>
|
||||
<thead>
|
||||
<tr>
|
||||
<th v-for="(column, i) in tableData.columns || []" :key="column">
|
||||
<n8n-tooltip placement="bottom-start" :disabled="!mappingEnabled || showHintWithDelay" :open-delay="1000">
|
||||
<div slot="content" v-html="$locale.baseText('dataMapping.dragColumnToFieldHint')"></div>
|
||||
<Draggable type="mapping" :data="getExpression(column)" :disabled="!mappingEnabled" @dragstart="onDragStart" @dragend="(column) => onDragEnd(column)">
|
||||
<n8n-tooltip
|
||||
placement="bottom-start"
|
||||
:disabled="!mappingEnabled || showHintWithDelay"
|
||||
:open-delay="1000"
|
||||
>
|
||||
<div
|
||||
slot="content"
|
||||
v-html="$locale.baseText('dataMapping.dragColumnToFieldHint')"
|
||||
></div>
|
||||
<Draggable
|
||||
type="mapping"
|
||||
:data="getExpression(column)"
|
||||
:disabled="!mappingEnabled"
|
||||
@dragstart="onDragStart"
|
||||
@dragend="(column) => onDragEnd(column, 'column')"
|
||||
>
|
||||
<template v-slot:preview="{ canDrop }">
|
||||
<div :class="[$style.dragPill, canDrop ? $style.droppablePill: $style.defaultPill]">
|
||||
{{ $locale.baseText('dataMapping.mapSpecificColumnToField', { interpolate: { name: shorten(column, 16, 2) } }) }}
|
||||
<div
|
||||
:class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]"
|
||||
>
|
||||
{{
|
||||
$locale.baseText('dataMapping.mapSpecificColumnToField', {
|
||||
interpolate: { name: shorten(column, 16, 2) },
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
<template v-slot="{ isDragging }">
|
||||
|
@ -27,14 +48,32 @@
|
|||
:class="{
|
||||
[$style.header]: true,
|
||||
[$style.draggableHeader]: mappingEnabled,
|
||||
[$style.activeHeader]: (i === activeColumn || forceShowGrip) && mappingEnabled,
|
||||
[$style.activeHeader]:
|
||||
(i === activeColumn || forceShowGrip) && mappingEnabled,
|
||||
[$style.draggingHeader]: isDragging,
|
||||
}"
|
||||
>
|
||||
<span>{{ column || " " }}</span>
|
||||
<n8n-tooltip v-if="mappingEnabled" placement="bottom-start" :manual="true" :value="i === 0 && showHintWithDelay">
|
||||
<div v-if="focusedMappableInput" slot="content" v-html="$locale.baseText('dataMapping.tableHint', { interpolate: { name: focusedMappableInput } })"></div>
|
||||
<div v-else slot="content" v-html="$locale.baseText('dataMapping.dragColumnToFieldHint')"></div>
|
||||
<span>{{ column || ' ' }}</span>
|
||||
<n8n-tooltip
|
||||
v-if="mappingEnabled"
|
||||
placement="bottom-start"
|
||||
:manual="true"
|
||||
:value="i === 0 && showHintWithDelay"
|
||||
>
|
||||
<div
|
||||
v-if="focusedMappableInput"
|
||||
slot="content"
|
||||
v-html="
|
||||
$locale.baseText('dataMapping.tableHint', {
|
||||
interpolate: { name: focusedMappableInput },
|
||||
})
|
||||
"
|
||||
></div>
|
||||
<div
|
||||
v-else
|
||||
slot="content"
|
||||
v-html="$locale.baseText('dataMapping.dragColumnToFieldHint')"
|
||||
></div>
|
||||
<div :class="$style.dragButton">
|
||||
<font-awesome-icon icon="grip-vertical" />
|
||||
</div>
|
||||
|
@ -44,9 +83,33 @@
|
|||
</Draggable>
|
||||
</n8n-tooltip>
|
||||
</th>
|
||||
<th :class="$style.tableRightMargin"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Draggable
|
||||
tag="tbody"
|
||||
type="mapping"
|
||||
targetDataKey="mappable"
|
||||
:disabled="!mappingEnabled"
|
||||
@dragstart="onCellDragStart"
|
||||
@dragend="onCellDragEnd"
|
||||
ref="draggable"
|
||||
>
|
||||
<template v-slot:preview="{ canDrop, el }">
|
||||
<div :class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]">
|
||||
{{
|
||||
$locale.baseText(
|
||||
tableData.data.length > 1
|
||||
? 'dataMapping.mapAllKeysToField'
|
||||
: 'dataMapping.mapSpecificColumnToField',
|
||||
{
|
||||
interpolate: { name: shorten(getPathNameFromTarget(el) || '', 16, 2) },
|
||||
},
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</template>
|
||||
<template>
|
||||
<tr v-for="(row, index1) in tableData.data" :key="index1">
|
||||
<td
|
||||
v-for="(data, index2) in row"
|
||||
|
@ -54,9 +117,40 @@
|
|||
:data-col="index2"
|
||||
@mouseenter="onMouseEnterCell"
|
||||
@mouseleave="onMouseLeaveCell"
|
||||
>{{ [null, undefined].includes(data) ? ' ' : data }}</td>
|
||||
:class="hasJsonInColumn(index2) ? $style.minColWidth : $style.limitColWidth"
|
||||
>
|
||||
<span v-if="isSimple(data)" :class="$style.value">{{
|
||||
[null, undefined].includes(data) ? ' ' : data
|
||||
}}</span>
|
||||
<n8n-tree :nodeClass="$style.nodeClass" v-else :value="data">
|
||||
<template v-slot:label="{ label, path }">
|
||||
<span
|
||||
@mouseenter="() => onMouseEnterKey(path, index2)"
|
||||
@mouseleave="onMouseLeaveKey"
|
||||
:class="{
|
||||
[$style.hoveringKey]: mappingEnabled && isHovering(path, index2),
|
||||
[$style.draggingKey]: isDraggingKey(path, index2),
|
||||
[$style.dataKey]: true,
|
||||
[$style.mappable]: mappingEnabled,
|
||||
}"
|
||||
data-target="mappable"
|
||||
:data-name="getCellPathName(path, index2)"
|
||||
:data-value="getCellExpression(path, index2)"
|
||||
:data-depth="path.length"
|
||||
>{{ label || $locale.baseText('runData.unnamedField') }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-slot:value="{ value }">
|
||||
<span :class="{ [$style.nestedValue]: true, [$style.empty]: isEmpty(value) }">{{
|
||||
getValueToRender(value)
|
||||
}}</span>
|
||||
</template>
|
||||
</n8n-tree>
|
||||
</td>
|
||||
<td :class="$style.tableRightMargin"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</template>
|
||||
</Draggable>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -64,6 +158,7 @@
|
|||
<script lang="ts">
|
||||
import { LOCAL_STORAGE_MAPPING_FLAG } from '@/constants';
|
||||
import { INodeUi, ITableData } from '@/Interface';
|
||||
import { GenericValue, IDataObject, INodeExecutionData } from 'n8n-workflow';
|
||||
import Vue from 'vue';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import Draggable from './Draggable.vue';
|
||||
|
@ -77,8 +172,8 @@ export default mixins(externalHooks).extend({
|
|||
node: {
|
||||
type: Object as () => INodeUi,
|
||||
},
|
||||
tableData: {
|
||||
type: Object as () => ITableData,
|
||||
inputData: {
|
||||
type: Object as () => INodeExecutionData[],
|
||||
},
|
||||
mappingEnabled: {
|
||||
type: Boolean,
|
||||
|
@ -102,6 +197,8 @@ export default mixins(externalHooks).extend({
|
|||
showHintWithDelay: false,
|
||||
forceShowGrip: false,
|
||||
draggedColumn: false,
|
||||
draggingPath: null as null | string,
|
||||
hoveringPath: null as null | string,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
|
@ -111,13 +208,30 @@ export default mixins(externalHooks).extend({
|
|||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
if (this.tableData && this.tableData.columns && this.$refs.draggable) {
|
||||
const tbody = (this.$refs.draggable as Vue).$refs.wrapper as HTMLElement;
|
||||
if (tbody) {
|
||||
this.$emit('mounted', {
|
||||
avgRowHeight: tbody.offsetHeight / this.tableData.data.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
focusedMappableInput (): string {
|
||||
tableData(): ITableData {
|
||||
return this.convertToTable(this.inputData);
|
||||
},
|
||||
focusedMappableInput(): string {
|
||||
return this.$store.getters['ui/focusedMappableInput'];
|
||||
},
|
||||
showHint (): boolean {
|
||||
return !this.draggedColumn && (this.showMappingHint || (!!this.focusedMappableInput && window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) !== 'true'));
|
||||
showHint(): boolean {
|
||||
return (
|
||||
!this.draggedColumn &&
|
||||
(this.showMappingHint ||
|
||||
(!!this.focusedMappableInput &&
|
||||
window.localStorage.getItem(LOCAL_STORAGE_MAPPING_FLAG) !== 'true'))
|
||||
);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -134,6 +248,17 @@ export default mixins(externalHooks).extend({
|
|||
onMouseLeaveCell() {
|
||||
this.activeColumn = -1;
|
||||
},
|
||||
onMouseEnterKey(path: string[], colIndex: number) {
|
||||
this.hoveringPath = this.getCellExpression(path, colIndex);
|
||||
},
|
||||
onMouseLeaveKey() {
|
||||
this.hoveringPath = null;
|
||||
},
|
||||
isHovering(path: string[], colIndex: number) {
|
||||
const expr = this.getCellExpression(path, colIndex);
|
||||
|
||||
return this.hoveringPath === expr;
|
||||
},
|
||||
getExpression(column: string) {
|
||||
if (!this.node) {
|
||||
return '';
|
||||
|
@ -145,12 +270,94 @@ export default mixins(externalHooks).extend({
|
|||
|
||||
return `{{ $node["${this.node.name}"].json["${column}"] }}`;
|
||||
},
|
||||
getPathNameFromTarget(el: HTMLElement) {
|
||||
if (!el) {
|
||||
return '';
|
||||
}
|
||||
return el.dataset.name;
|
||||
},
|
||||
getCellPathName(path: Array<string | number>, colIndex: number) {
|
||||
const lastKey = path[path.length - 1];
|
||||
if (typeof lastKey === 'string') {
|
||||
return lastKey;
|
||||
}
|
||||
if (path.length > 1) {
|
||||
const prevKey = path[path.length - 2];
|
||||
return `${prevKey}[${lastKey}]`;
|
||||
}
|
||||
const column = this.tableData.columns[colIndex];
|
||||
return `${column}[${lastKey}]`;
|
||||
},
|
||||
getCellExpression(path: Array<string | number>, colIndex: number) {
|
||||
if (!this.node) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const expr = path.reduce((accu: string, key: string | number) => {
|
||||
if (typeof key === 'number') {
|
||||
return `${accu}[${key}]`;
|
||||
}
|
||||
|
||||
return `${accu}["${key}"]`;
|
||||
}, '');
|
||||
const column = this.tableData.columns[colIndex];
|
||||
|
||||
if (this.distanceFromActive === 1) {
|
||||
return `{{ $json["${column}"]${expr} }}`;
|
||||
}
|
||||
|
||||
return `{{ $node["${this.node.name}"].json["${column}"]${expr} }}`;
|
||||
},
|
||||
isEmpty(value: unknown) {
|
||||
return (
|
||||
value === '' ||
|
||||
(Array.isArray(value) && value.length === 0) ||
|
||||
(typeof value === 'object' && value !== null && Object.keys(value).length === 0)
|
||||
);
|
||||
},
|
||||
getValueToRender(value: unknown) {
|
||||
if (value === '') {
|
||||
return this.$locale.baseText('runData.emptyString');
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
return value.replaceAll('\n', '\\n');
|
||||
}
|
||||
|
||||
if (Array.isArray(value) && value.length === 0) {
|
||||
return this.$locale.baseText('runData.emptyArray');
|
||||
}
|
||||
|
||||
if (typeof value === 'object' && value !== null && Object.keys(value).length === 0) {
|
||||
return this.$locale.baseText('runData.emptyObject');
|
||||
}
|
||||
|
||||
return value;
|
||||
},
|
||||
onDragStart() {
|
||||
this.draggedColumn = true;
|
||||
|
||||
this.$store.commit('ui/resetMappingTelemetry');
|
||||
},
|
||||
onDragEnd(column: string) {
|
||||
onCellDragStart(el: HTMLElement) {
|
||||
if (el && el.dataset.value) {
|
||||
this.draggingPath = el.dataset.value;
|
||||
}
|
||||
|
||||
this.onDragStart();
|
||||
},
|
||||
onCellDragEnd(el: HTMLElement) {
|
||||
this.draggingPath = null;
|
||||
|
||||
this.onDragEnd(el.dataset.name || '', 'tree', el.dataset.depth || '0');
|
||||
},
|
||||
isDraggingKey(path: Array<string | number>, colIndex: number) {
|
||||
if (!this.draggingPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
return this.draggingPath === this.getCellExpression(path, colIndex);
|
||||
},
|
||||
onDragEnd(column: string, src: string, depth = '0') {
|
||||
setTimeout(() => {
|
||||
const mappingTelemetry = this.$store.getters['ui/mappingTelemetry'];
|
||||
const telemetryPayload = {
|
||||
|
@ -159,8 +366,9 @@ export default mixins(externalHooks).extend({
|
|||
src_nodes_back: this.distanceFromActive,
|
||||
src_run_index: this.runIndex,
|
||||
src_runs_total: this.totalRuns,
|
||||
src_field_nest_level: parseInt(depth, 10),
|
||||
src_view: 'table',
|
||||
src_element: 'column',
|
||||
src_element: src,
|
||||
success: false,
|
||||
...mappingTelemetry,
|
||||
};
|
||||
|
@ -170,14 +378,82 @@ export default mixins(externalHooks).extend({
|
|||
this.$telemetry.track('User dragged data for mapping', telemetryPayload);
|
||||
}, 1000); // ensure dest data gets set if drop
|
||||
},
|
||||
isSimple(data: unknown): boolean {
|
||||
return typeof data !== 'object';
|
||||
},
|
||||
hasJsonInColumn(colIndex: number): boolean {
|
||||
return this.tableData.hasJson[this.tableData.columns[colIndex]];
|
||||
},
|
||||
convertToTable(inputData: INodeExecutionData[]): ITableData {
|
||||
const tableData: GenericValue[][] = [];
|
||||
const tableColumns: string[] = [];
|
||||
let leftEntryColumns: string[], entryRows: GenericValue[];
|
||||
// Go over all entries
|
||||
let entry: IDataObject;
|
||||
const hasJson: { [key: string]: boolean } = {};
|
||||
inputData.forEach((data) => {
|
||||
if (!data.hasOwnProperty('json')) {
|
||||
return;
|
||||
}
|
||||
entry = data.json;
|
||||
|
||||
// Go over all keys of entry
|
||||
entryRows = [];
|
||||
leftEntryColumns = Object.keys(entry);
|
||||
|
||||
// Go over all the already existing column-keys
|
||||
tableColumns.forEach((key) => {
|
||||
if (entry.hasOwnProperty(key)) {
|
||||
// Entry does have key so add its value
|
||||
entryRows.push(entry[key]);
|
||||
// Remove key so that we know that it got added
|
||||
leftEntryColumns.splice(leftEntryColumns.indexOf(key), 1);
|
||||
|
||||
hasJson[key] = typeof entry[key] === 'object' || hasJson[key] || false;
|
||||
} else {
|
||||
// Entry does not have key so add null
|
||||
entryRows.push(null);
|
||||
}
|
||||
});
|
||||
|
||||
// Go over all the columns the entry has but did not exist yet
|
||||
leftEntryColumns.forEach((key) => {
|
||||
// Add the key for all runs in the future
|
||||
tableColumns.push(key);
|
||||
// Add the value
|
||||
entryRows.push(entry[key]);
|
||||
hasJson[key] = hasJson[key] || (entry[key] === 'object' && Object.keys(entry[key] || {}).length > 0);
|
||||
});
|
||||
|
||||
// Add the data of the entry
|
||||
tableData.push(entryRows);
|
||||
});
|
||||
|
||||
// Make sure that all entry-rows have the same length
|
||||
tableData.forEach((entryRows) => {
|
||||
if (tableColumns.length > entryRows.length) {
|
||||
// Has to less entries so add the missing ones
|
||||
entryRows.push.apply(entryRows, new Array(tableColumns.length - entryRows.length));
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
hasJson,
|
||||
columns: tableColumns,
|
||||
data: tableData,
|
||||
};
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
focusedMappableInput (curr: boolean) {
|
||||
setTimeout(() => {
|
||||
focusedMappableInput(curr: boolean) {
|
||||
setTimeout(
|
||||
() => {
|
||||
this.forceShowGrip = !!this.focusedMappableInput;
|
||||
}, curr? 300: 150);
|
||||
},
|
||||
showHint (curr: boolean, prev: boolean) {
|
||||
curr ? 300 : 150,
|
||||
);
|
||||
},
|
||||
showHint(curr: boolean, prev: boolean) {
|
||||
if (curr) {
|
||||
setTimeout(() => {
|
||||
this.showHintWithDelay = this.showHint;
|
||||
|
@ -185,8 +461,7 @@ export default mixins(externalHooks).extend({
|
|||
this.$telemetry.track('User viewed data mapping tooltip', { type: 'param focus' });
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
this.showHintWithDelay = false;
|
||||
}
|
||||
},
|
||||
|
@ -198,8 +473,7 @@ export default mixins(externalHooks).extend({
|
|||
.table {
|
||||
border-collapse: separate;
|
||||
text-align: left;
|
||||
width: calc(100% - var(--spacing-s));
|
||||
margin-right: var(--spacing-s);
|
||||
width: calc(100%);
|
||||
font-size: var(--font-size-s);
|
||||
|
||||
th {
|
||||
|
@ -209,15 +483,15 @@ export default mixins(externalHooks).extend({
|
|||
border-left: var(--border-base);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
max-width: 300px;
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
||||
td {
|
||||
padding: var(--spacing-2xs);
|
||||
vertical-align: top;
|
||||
padding: var(--spacing-2xs) var(--spacing-2xs) var(--spacing-2xs) var(--spacing-3xs);
|
||||
border-bottom: var(--border-base);
|
||||
border-left: var(--border-base);
|
||||
overflow-wrap: break-word;
|
||||
max-width: 300px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
|
@ -227,6 +501,10 @@ export default mixins(externalHooks).extend({
|
|||
}
|
||||
}
|
||||
|
||||
.nodeClass {
|
||||
margin-bottom: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.emptyCell {
|
||||
height: 32px;
|
||||
}
|
||||
|
@ -288,4 +566,54 @@ export default mixins(externalHooks).extend({
|
|||
transform: translate(-50%, -100%);
|
||||
box-shadow: 0px 2px 6px rgba(68, 28, 23, 0.2);
|
||||
}
|
||||
|
||||
.dataKey {
|
||||
color: var(--color-text-dark);
|
||||
line-height: 1.7;
|
||||
font-weight: var(--font-weight-bold);
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: 0 var(--spacing-5xs) 0 var(--spacing-5xs);
|
||||
margin-right: var(--spacing-5xs);
|
||||
}
|
||||
|
||||
.value {
|
||||
line-height: var(--font-line-height-regular);
|
||||
}
|
||||
|
||||
.nestedValue {
|
||||
composes: value;
|
||||
margin-left: var(--spacing-4xs);
|
||||
}
|
||||
|
||||
.mappable {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: var(--color-danger);
|
||||
}
|
||||
|
||||
.limitColWidth {
|
||||
max-width: 300px;
|
||||
}
|
||||
|
||||
.minColWidth {
|
||||
min-width: 240px;
|
||||
}
|
||||
|
||||
.hoveringKey {
|
||||
background-color: var(--color-foreground-base);
|
||||
}
|
||||
|
||||
.draggingKey {
|
||||
background-color: var(--color-primary-tint-2);
|
||||
}
|
||||
|
||||
.tableRightMargin {
|
||||
// becomes necessary with large tables
|
||||
width: var(--spacing-s);
|
||||
border-right: none !important;
|
||||
border-top: none !important;
|
||||
border-bottom: none !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -187,8 +187,8 @@ const module: Module<IUiState, IRootState> = {
|
|||
getPanelDisplayMode: (state: IUiState) => {
|
||||
return (panel: 'input' | 'output') => state.ndv[panel].displayMode;
|
||||
},
|
||||
inputPanelDispalyMode: (state: IUiState) => state.ndv.input.displayMode,
|
||||
outputPanelDispalyMode: (state: IUiState) => state.ndv.output.displayMode,
|
||||
inputPanelDisplayMode: (state: IUiState) => state.ndv.input.displayMode,
|
||||
outputPanelDisplayMode: (state: IUiState) => state.ndv.output.displayMode,
|
||||
outputPanelEditMode: (state: IUiState): IUiState['ndv']['output']['editMode'] => state.ndv.output.editMode,
|
||||
mainPanelPosition: (state: IUiState) => state.mainPanelPosition,
|
||||
getFakeDoorFeatures: (state: IUiState) => state.fakeDoorFeatures,
|
||||
|
|
|
@ -162,11 +162,12 @@
|
|||
"dataDisplay.nodeDocumentation": "Node Documentation",
|
||||
"dataDisplay.openDocumentationFor": "Open {nodeTypeDisplayName} documentation",
|
||||
"dataMapping.dragColumnToFieldHint": "<img src='/static/data-mapping-gif.gif'/> Drag onto a field to map column to that field",
|
||||
"dataMapping.dragFromPreviousHint": "Map data from previous nodes to <b>{name}</b><br/> by first clicking this button",
|
||||
"dataMapping.dragFromPreviousHint": "Map data from previous nodes to <b>{name}</b> by first clicking this button",
|
||||
"dataMapping.success.title": "You just mapped some data!",
|
||||
"dataMapping.success.moreInfo": "Check out our <a href=\"https://docs.n8n.io/data/data-mapping\" target=\"_blank\">docs</a> for more details on mapping data in n8n",
|
||||
"dataMapping.tableHint": "<img src='/static/data-mapping-gif.gif'/> Drag a column onto <b>{name}</b> to map it",
|
||||
"dataMapping.mapSpecificColumnToField": "Map {name} to field",
|
||||
"dataMapping.mapSpecificColumnToField": "Map '{name}' to a field",
|
||||
"dataMapping.mapAllKeysToField": "Map every '{name}' to a field",
|
||||
"displayWithChange.cancelEdit": "Cancel Edit",
|
||||
"displayWithChange.clickToChange": "Click to Change",
|
||||
"displayWithChange.setValue": "Set Value",
|
||||
|
@ -672,6 +673,10 @@
|
|||
"pushConnection.pollingNode.dataNotFound": "No {service} data found",
|
||||
"pushConnection.pollingNode.dataNotFound.message": "We didn’t find any data in {service} to simulate an event. Please create one in {service} and try again.",
|
||||
"runData.emptyItemHint": "This is an item, but it's empty.",
|
||||
"runData.emptyArray": "[empty array]",
|
||||
"runData.emptyString": "[empty]",
|
||||
"runData.emptyObject": "[empty object]",
|
||||
"runData.unnamedField": "[Unnamed field]",
|
||||
"runData.switchToBinary.info": "This item only has",
|
||||
"runData.switchToBinary.binary": "binary data",
|
||||
"runData.linking.hint": "Link displayed input and output runs",
|
||||
|
|
Loading…
Reference in a new issue