feat add item/header component

This commit is contained in:
r00gm 2024-11-12 12:38:28 +01:00
parent bc92b8a0f8
commit 6b6157f4f7
No known key found for this signature in database
2 changed files with 188 additions and 285 deletions

View 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>

View file

@ -1,244 +1,76 @@
<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 { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
type Props = {
schema: Schema;
level: number;
parent: Schema | null;
subKey: string;
paneType: 'input' | 'output';
mappingEnabled: boolean;
draggingPath: string;
distanceFromActive?: number;
node: INodeUi | null;
search: string;
title?: string;
path?: string;
level?: number;
depth?: number;
expression?: string;
value?: string;
id: string;
icon?: string;
collapsable?: boolean;
nodeType?: string;
type: 'item';
highlight?: boolean;
draggable?: boolean;
collapsed?: boolean;
search?: string;
};
const props = defineProps<Props>();
const isSchemaValueArray = computed(() => Array.isArray(props.schema.value));
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 '';
}
};
const emit = defineEmits<{
click: [];
}>();
</script>
<template>
<div :class="$style.item" data-test-id="run-data-schema-item">
<div :class="$style.itemContent">
<div
v-if="level > 0 || (level === 0 && !isSchemaValueArray)"
: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 class="schema-item" :class="{ draggable }" data-test-id="run-data-schema-item">
<div class="toggle-container">
<div v-if="collapsable" class="toggle" @click="emit('click')">
<FontAwesomeIcon icon="angle-down" :class="{ 'collapse-icon': true, collapsed }" />
</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>
</template>
<style lang="scss" module>
@import '@/styles/variables';
.item {
<style lang="css" scoped>
.schema-item {
display: flex;
flex-wrap: wrap;
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);
margin-left: calc(var(--spacing-l) * v-bind(level));
align-items: baseline;
flex-grow: 1;
min-width: 0;
padding-bottom: var(--spacing-2xs);
}
.sub {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
flex-basis: 100%;
scroll-margin: 64px;
.toggle-container {
min-width: var(--spacing-l);
min-height: 17px;
}
.innerSub {
display: inline-flex;
flex-direction: column;
order: -1;
min-width: 0;
.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);
}
}
}
}
.toggle {
cursor: pointer;
display: flex;
justify-content: center;
cursor: pointer;
font-size: var(--font-size-s);
}
.pill {
@ -251,83 +83,53 @@ const getIconBySchemaType = (type: Schema['type']): string => {
font-size: var(--font-size-2xs);
color: var(--color-text-dark);
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;
> span {
display: flex;
align-items: center;
> *:not(:first-child) {
margin-left: var(--spacing-3xs);
padding-left: var(--spacing-3xs);
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 {
display: block;
font-weight: var(--font-weight-normal);
font-size: var(--font-size-2xs);
overflow: hidden;
word-break: break-word;
.newLine {
font-family: var(--font-family-monospace);
color: var(--color-line-break);
padding-right: 2px;
}
margin-left: var(--spacing-2xs);
}
.toggle {
display: flex;
position: absolute;
padding: var(--spacing-4xs) var(--spacing-2xs);
left: 0;
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;
}
.collapse-icon {
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
}
.collapsed {
transform: rotateZ(-90deg);
}
</style>