mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat add item/header component
This commit is contained in:
parent
bc92b8a0f8
commit
6b6157f4f7
101
packages/editor-ui/src/components/RunDataSchemaHeader.vue
Normal file
101
packages/editor-ui/src/components/RunDataSchemaHeader.vue
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
import { type INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
info?: string;
|
||||||
|
collapsable: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
nodeType: INodeTypeDescription;
|
||||||
|
itemCount: number | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const isTrigger = computed(() => props.nodeType.group.includes('trigger'));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schema-header">
|
||||||
|
<div class="toggle">
|
||||||
|
<FontAwesomeIcon icon="angle-down" :class="{ 'collapse-icon': true, collapsed }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NodeIcon
|
||||||
|
class="icon"
|
||||||
|
:class="{ ['icon-trigger']: isTrigger }"
|
||||||
|
:node-type="nodeType"
|
||||||
|
:size="12"
|
||||||
|
/>
|
||||||
|
<div class="title">
|
||||||
|
{{ title }}
|
||||||
|
<span v-if="info" class="info">{{ info }}</span>
|
||||||
|
</div>
|
||||||
|
<FontAwesomeIcon v-if="isTrigger" class="trigger-icon" icon="bolt" size="xs" />
|
||||||
|
<div v-if="itemCount" class="item-count" data-test-id="run-data-schema-node-item-count">
|
||||||
|
{{ i18n.baseText('ndv.output.items', { interpolate: { count: itemCount } }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.schema-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: var(--spacing-2xs);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.collapse-icon {
|
||||||
|
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
}
|
||||||
|
.collapsed {
|
||||||
|
transform: rotateZ(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-3xs);
|
||||||
|
border: 1px solid var(--color-foreground-light);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
margin-right: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-trigger {
|
||||||
|
border-radius: 16px 4px 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-icon {
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,244 +1,76 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
|
||||||
import type { INodeUi, Schema } from '@/Interface';
|
|
||||||
import { checkExhaustive } from '@/utils/typeGuards';
|
|
||||||
import { shorten } from '@/utils/typesUtils';
|
|
||||||
import { getMappedExpression } from '@/utils/mappingUtils';
|
|
||||||
import TextWithHighlights from './TextWithHighlights.vue';
|
import TextWithHighlights from './TextWithHighlights.vue';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
schema: Schema;
|
title?: string;
|
||||||
level: number;
|
path?: string;
|
||||||
parent: Schema | null;
|
level?: number;
|
||||||
subKey: string;
|
depth?: number;
|
||||||
paneType: 'input' | 'output';
|
expression?: string;
|
||||||
mappingEnabled: boolean;
|
value?: string;
|
||||||
draggingPath: string;
|
id: string;
|
||||||
distanceFromActive?: number;
|
icon?: string;
|
||||||
node: INodeUi | null;
|
collapsable?: boolean;
|
||||||
search: string;
|
nodeType?: string;
|
||||||
|
type: 'item';
|
||||||
|
highlight?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
collapsed?: boolean;
|
||||||
|
search?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
const isSchemaValueArray = computed(() => Array.isArray(props.schema.value));
|
click: [];
|
||||||
const schemaArray = computed(
|
}>();
|
||||||
() => (isSchemaValueArray.value ? props.schema.value : []) as Schema[],
|
|
||||||
);
|
|
||||||
const isSchemaParentTypeArray = computed(() => props.parent?.type === 'array');
|
|
||||||
|
|
||||||
const key = computed((): string | undefined => {
|
|
||||||
return isSchemaParentTypeArray.value ? `[${props.schema.key}]` : props.schema.key;
|
|
||||||
});
|
|
||||||
const schemaName = computed(() =>
|
|
||||||
isSchemaParentTypeArray.value ? `${props.schema.type}[${props.schema.key}]` : props.schema.key,
|
|
||||||
);
|
|
||||||
|
|
||||||
const text = computed(() =>
|
|
||||||
Array.isArray(props.schema.value) ? '' : shorten(props.schema.value, 600, 0),
|
|
||||||
);
|
|
||||||
|
|
||||||
const dragged = computed(() => props.draggingPath === props.schema.path);
|
|
||||||
|
|
||||||
const getJsonParameterPath = (path: string): string =>
|
|
||||||
getMappedExpression({
|
|
||||||
nodeName: props.node!.name,
|
|
||||||
distanceFromActive: props.distanceFromActive ?? 1,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
|
|
||||||
const getIconBySchemaType = (type: Schema['type']): string => {
|
|
||||||
switch (type) {
|
|
||||||
case 'object':
|
|
||||||
return 'cube';
|
|
||||||
case 'array':
|
|
||||||
return 'list';
|
|
||||||
case 'string':
|
|
||||||
case 'null':
|
|
||||||
return 'font';
|
|
||||||
case 'number':
|
|
||||||
return 'hashtag';
|
|
||||||
case 'boolean':
|
|
||||||
return 'check-square';
|
|
||||||
case 'function':
|
|
||||||
return 'code';
|
|
||||||
case 'bigint':
|
|
||||||
return 'calculator';
|
|
||||||
case 'symbol':
|
|
||||||
return 'sun';
|
|
||||||
case 'undefined':
|
|
||||||
return 'ban';
|
|
||||||
default:
|
|
||||||
checkExhaustive(type);
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.item" data-test-id="run-data-schema-item">
|
<div class="schema-item" :class="{ draggable }" data-test-id="run-data-schema-item">
|
||||||
<div :class="$style.itemContent">
|
<div class="toggle-container">
|
||||||
<div
|
<div v-if="collapsable" class="toggle" @click="emit('click')">
|
||||||
v-if="level > 0 || (level === 0 && !isSchemaValueArray)"
|
<FontAwesomeIcon icon="angle-down" :class="{ 'collapse-icon': true, collapsed }" />
|
||||||
:title="schema.type"
|
|
||||||
:class="{
|
|
||||||
[$style.pill]: true,
|
|
||||||
[$style.mappable]: mappingEnabled,
|
|
||||||
[$style.highlight]: dragged,
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
:class="$style.label"
|
|
||||||
:data-value="getJsonParameterPath(schema.path)"
|
|
||||||
:data-name="schemaName"
|
|
||||||
:data-path="schema.path"
|
|
||||||
:data-depth="level"
|
|
||||||
data-target="mappable"
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon :icon="getIconBySchemaType(schema.type)" size="sm" />
|
|
||||||
<TextWithHighlights
|
|
||||||
v-if="isSchemaParentTypeArray"
|
|
||||||
:content="props.parent?.key"
|
|
||||||
:search="props.search"
|
|
||||||
/>
|
|
||||||
<TextWithHighlights
|
|
||||||
v-if="key"
|
|
||||||
:class="{ [$style.arrayIndex]: isSchemaParentTypeArray }"
|
|
||||||
:content="key"
|
|
||||||
:search="props.search"
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<span v-if="text" :class="$style.text" data-test-id="run-data-schema-item-value">
|
|
||||||
<template v-for="(line, index) in text.split('\n')" :key="`line-${index}`">
|
|
||||||
<span v-if="index > 0" :class="$style.newLine">\n</span>
|
|
||||||
<TextWithHighlights :content="line" :search="props.search" />
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input v-if="level > 0 && isSchemaValueArray" :id="subKey" type="checkbox" inert checked />
|
|
||||||
<label v-if="level > 0 && isSchemaValueArray" :class="$style.toggle" :for="subKey">
|
|
||||||
<FontAwesomeIcon icon="angle-right" />
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div v-if="isSchemaValueArray" :class="$style.sub">
|
|
||||||
<div :class="$style.innerSub">
|
|
||||||
<RunDataSchemaItem
|
|
||||||
v-for="s in schemaArray"
|
|
||||||
:key="s.key ?? s.type"
|
|
||||||
:schema="s"
|
|
||||||
:level="level + 1"
|
|
||||||
:parent="schema"
|
|
||||||
:pane-type="paneType"
|
|
||||||
:sub-key="`${subKey}-${s.key ?? s.type}`"
|
|
||||||
:mapping-enabled="mappingEnabled"
|
|
||||||
:dragging-path="draggingPath"
|
|
||||||
:distance-from-active="distanceFromActive"
|
|
||||||
:node="node"
|
|
||||||
:search="search"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="title"
|
||||||
|
:data-name="title"
|
||||||
|
:data-path="path"
|
||||||
|
:data-depth="depth"
|
||||||
|
:data-nest-level="level"
|
||||||
|
:data-value="expression"
|
||||||
|
:data-node-type="nodeType"
|
||||||
|
data-target="mappable"
|
||||||
|
class="pill"
|
||||||
|
:class="{ 'pill--highlight': highlight }"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon v-if="icon" class="type-icon" :icon size="sm" />
|
||||||
|
<TextWithHighlights class="title" :content="title" :search="props.search" />
|
||||||
|
</div>
|
||||||
|
<TextWithHighlights class="text" :content="value" :search="props.search" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="css" scoped>
|
||||||
@import '@/styles/variables';
|
.schema-item {
|
||||||
|
|
||||||
.item {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
margin-left: calc(var(--spacing-l) * v-bind(level));
|
||||||
align-items: center;
|
|
||||||
line-height: var(--font-line-height-loose);
|
|
||||||
position: relative;
|
|
||||||
column-gap: var(--spacing-2xs);
|
|
||||||
|
|
||||||
+ .item {
|
|
||||||
margin-top: var(--spacing-2xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.item {
|
|
||||||
padding-left: var(--spacing-l);
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
display: none;
|
|
||||||
|
|
||||||
~ .sub {
|
|
||||||
transition:
|
|
||||||
grid-template-rows 0.2s $ease-out-expo,
|
|
||||||
opacity 0.2s $ease-out-expo,
|
|
||||||
transform 0.2s $ease-out-expo;
|
|
||||||
transform: translateX(-8px);
|
|
||||||
opacity: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
|
|
||||||
.innerSub {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked {
|
|
||||||
~ .toggle svg {
|
|
||||||
transform: rotate(90deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
~ .sub {
|
|
||||||
transform: translateX(0);
|
|
||||||
opacity: 1;
|
|
||||||
grid-template-rows: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.itemContent {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--spacing-2xs);
|
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
flex-grow: 1;
|
padding-bottom: var(--spacing-2xs);
|
||||||
min-width: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sub {
|
.toggle-container {
|
||||||
display: grid;
|
min-width: var(--spacing-l);
|
||||||
grid-template-rows: 0fr;
|
min-height: 17px;
|
||||||
overflow: hidden;
|
|
||||||
flex-basis: 100%;
|
|
||||||
scroll-margin: 64px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.innerSub {
|
.toggle {
|
||||||
display: inline-flex;
|
cursor: pointer;
|
||||||
flex-direction: column;
|
display: flex;
|
||||||
order: -1;
|
justify-content: center;
|
||||||
min-width: 0;
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
.innerSub > div:first-child {
|
|
||||||
margin-top: var(--spacing-2xs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.highlightSchema) {
|
|
||||||
.pill.mappable {
|
|
||||||
&,
|
|
||||||
&:hover,
|
|
||||||
span,
|
|
||||||
&:hover span span {
|
|
||||||
color: var(--color-primary);
|
|
||||||
border-color: var(--color-primary-tint-1);
|
|
||||||
background-color: var(--color-primary-tint-3);
|
|
||||||
|
|
||||||
svg {
|
|
||||||
path {
|
|
||||||
fill: var(--color-primary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
|
@ -251,83 +83,53 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
color: var(--color-text-dark);
|
color: var(--color-text-dark);
|
||||||
max-width: 50%;
|
max-width: 50%;
|
||||||
|
|
||||||
path {
|
|
||||||
fill: var(--color-text-light);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.mappable {
|
|
||||||
cursor: grab;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
&,
|
|
||||||
span span {
|
|
||||||
background-color: var(--color-background-light);
|
|
||||||
border-color: var(--color-foreground-base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.label {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
> *:not(:first-child) {
|
||||||
> span {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
margin-left: var(--spacing-3xs);
|
margin-left: var(--spacing-3xs);
|
||||||
padding-left: var(--spacing-3xs);
|
padding-left: var(--spacing-3xs);
|
||||||
border-left: 1px solid var(--color-foreground-light);
|
border-left: 1px solid var(--color-foreground-light);
|
||||||
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
span {
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.arrayIndex {
|
|
||||||
border: 0;
|
|
||||||
padding-left: 0;
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.draggable .pill.pill--highlight {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary-tint-1);
|
||||||
|
background-color: var(--color-primary-tint-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill.pill--highlight .type-icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill:hover {
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
border-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
display: block;
|
|
||||||
font-weight: var(--font-weight-normal);
|
font-weight: var(--font-weight-normal);
|
||||||
font-size: var(--font-size-2xs);
|
font-size: var(--font-size-2xs);
|
||||||
overflow: hidden;
|
margin-left: var(--spacing-2xs);
|
||||||
word-break: break-word;
|
|
||||||
|
|
||||||
.newLine {
|
|
||||||
font-family: var(--font-family-monospace);
|
|
||||||
color: var(--color-line-break);
|
|
||||||
padding-right: 2px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.toggle {
|
.collapse-icon {
|
||||||
display: flex;
|
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
position: absolute;
|
}
|
||||||
padding: var(--spacing-4xs) var(--spacing-2xs);
|
.collapsed {
|
||||||
left: 0;
|
transform: rotateZ(-90deg);
|
||||||
top: 0;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
cursor: pointer;
|
|
||||||
user-select: none;
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: var(--font-size-s);
|
|
||||||
overflow: hidden;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
transition: transform 0.2s $ease-out-expo;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue