mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
P0: implement schema preview frontend with hardcoded schema
This commit is contained in:
parent
41e9e39b5b
commit
280cc619c5
|
@ -4,6 +4,7 @@ import {
|
|||
CRON_NODE_TYPE,
|
||||
INTERVAL_NODE_TYPE,
|
||||
MANUAL_TRIGGER_NODE_TYPE,
|
||||
PREVIEW_SCHEMAS,
|
||||
START_NODE_TYPE,
|
||||
} from '@/constants';
|
||||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
|
@ -185,6 +186,10 @@ const currentNode = computed(() => {
|
|||
return workflowsStore.getNodeByName(props.currentNodeName ?? '');
|
||||
});
|
||||
|
||||
const hasSchemaPreview = computed(() => {
|
||||
return Boolean(currentNode.value && PREVIEW_SCHEMAS.hasOwnProperty(currentNode.value.type));
|
||||
});
|
||||
|
||||
const connectedCurrentNodeOutputs = computed(() => {
|
||||
const search = parentNodes.value.find(({ name }) => name === props.currentNodeName);
|
||||
return search?.indicies;
|
||||
|
@ -428,9 +433,22 @@ function activatePane() {
|
|||
@execute="onNodeExecute"
|
||||
/>
|
||||
</N8nTooltip>
|
||||
<N8nText v-if="!readOnly" tag="div" size="small">
|
||||
<N8nText v-if="!readOnly && !hasSchemaPreview" tag="div" size="small">
|
||||
{{ i18n.baseText('ndv.input.noOutputData.hint') }}
|
||||
</N8nText>
|
||||
|
||||
<div v-if="!readOnly && hasSchemaPreview" :class="$style.schemaPreview">
|
||||
<N8nText tag="div" size="small">
|
||||
{{ i18n.baseText('ndv.input.noOutputData.or') }}
|
||||
</N8nText>
|
||||
<N8nText tag="div" size="small">
|
||||
<i18n-t keypath="ndv.input.noOutputData.schemaPreviewHint">
|
||||
<template #schema>
|
||||
<b>{{ i18n.baseText('runData.schema') }}</b>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</N8nText>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else :class="$style.notConnected">
|
||||
<div>
|
||||
|
@ -534,4 +552,10 @@ function activatePane() {
|
|||
font-size: var(--font-size-s);
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
.schemaPreview {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
gap: var(--spacing-2xs);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -34,6 +34,7 @@ import {
|
|||
MAX_DISPLAY_DATA_SIZE,
|
||||
MAX_DISPLAY_DATA_SIZE_SCHEMA_VIEW,
|
||||
NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND,
|
||||
PREVIEW_SCHEMAS,
|
||||
TEST_PIN_DATA,
|
||||
} from '@/constants';
|
||||
|
||||
|
@ -274,6 +275,10 @@ const hasNodeRun = computed(() =>
|
|||
),
|
||||
);
|
||||
|
||||
const hasPreviewSchema = computed(() =>
|
||||
Boolean(node.value && PREVIEW_SCHEMAS.hasOwnProperty(node.value.type)),
|
||||
);
|
||||
|
||||
const isArtificialRecoveredEventItem = computed(
|
||||
() => rawInputData.value?.[0]?.json?.isArtificialRecoveredEventItem,
|
||||
);
|
||||
|
@ -1318,7 +1323,8 @@ defineExpose({ enterEditMode });
|
|||
|
||||
<N8nRadioButtons
|
||||
v-show="
|
||||
hasNodeRun && (inputData.length || binaryData.length || search) && !editMode.enabled
|
||||
hasPreviewSchema ||
|
||||
(hasNodeRun && (inputData.length || binaryData.length || search) && !editMode.enabled)
|
||||
"
|
||||
:model-value="displayMode"
|
||||
:options="displayModes"
|
||||
|
@ -1562,7 +1568,7 @@ defineExpose({ enterEditMode });
|
|||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="!hasNodeRun && !(displaysMultipleNodes && node?.disabled)"
|
||||
v-else-if="!hasNodeRun && !(displaysMultipleNodes && (node?.disabled || hasPreviewSchema))"
|
||||
:class="$style.center"
|
||||
>
|
||||
<slot name="node-not-run"></slot>
|
||||
|
@ -1734,7 +1740,7 @@ defineExpose({ enterEditMode });
|
|||
<LazyRunDataHtml :input-html="inputHtml" />
|
||||
</Suspense>
|
||||
|
||||
<Suspense v-else-if="hasNodeRun && isSchemaView">
|
||||
<Suspense v-else-if="(hasNodeRun || hasPreviewSchema) && isSchemaView">
|
||||
<LazyRunDataSchema
|
||||
:nodes="nodes"
|
||||
:mapping-enabled="mappingEnabled"
|
||||
|
|
|
@ -22,6 +22,8 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||
import { executionDataToJson } from '@/utils/nodeTypesUtils';
|
||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||
import { useDebounce } from '@/composables/useDebounce';
|
||||
import { PREVIEW_SCHEMAS, DATA_EDITING_DOCS_URL } from '@/constants';
|
||||
import { N8nNotice } from 'n8n-design-system';
|
||||
|
||||
type Props = {
|
||||
nodes?: IConnectedNode[];
|
||||
|
@ -46,6 +48,7 @@ type SchemaNode = {
|
|||
connectedOutputIndexes: number[];
|
||||
itemsCount: number | null;
|
||||
schema: Schema | null;
|
||||
isPreview: boolean;
|
||||
};
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
|
@ -64,7 +67,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|||
|
||||
const draggingPath = ref<string>('');
|
||||
const nodesOpen = ref<Partial<Record<string, boolean>>>({});
|
||||
const nodesData = ref<Partial<Record<string, { schema: Schema; itemsCount: number }>>>({});
|
||||
const nodesData = ref<
|
||||
Partial<Record<string, { schema: Schema; itemsCount: number; isPreview: boolean }>>
|
||||
>({});
|
||||
const nodesLoading = ref<Partial<Record<string, boolean>>>({});
|
||||
const disableScrollInView = ref(false);
|
||||
|
||||
|
@ -90,7 +95,10 @@ const nodes = computed(() => {
|
|||
if (!fullNode) return null;
|
||||
|
||||
const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion);
|
||||
const { itemsCount, schema } = nodesData.value[node.name] ?? {
|
||||
|
||||
if (!nodeType) return null;
|
||||
|
||||
const { itemsCount, schema, isPreview } = nodesData.value[node.name] ?? {
|
||||
itemsCount: null,
|
||||
schema: null,
|
||||
};
|
||||
|
@ -102,8 +110,9 @@ const nodes = computed(() => {
|
|||
itemsCount,
|
||||
nodeType,
|
||||
schema: schema ? filterSchema(schema, props.search) : null,
|
||||
loading: nodesLoading.value[node.name],
|
||||
open: nodesOpen.value[node.name],
|
||||
loading: nodesLoading.value[node.name] ?? false,
|
||||
open: nodesOpen.value[node.name] ?? false,
|
||||
isPreview,
|
||||
};
|
||||
})
|
||||
.filter((node): node is SchemaNode => !!(node?.node && node.nodeType));
|
||||
|
@ -147,7 +156,7 @@ const noNodesOpen = computed(() => nodes.value.every((node) => !node.open));
|
|||
|
||||
const loadNodeData = async ({ node, connectedOutputIndexes }: SchemaNode) => {
|
||||
const pinData = workflowsStore.pinDataByNodeName(node.name);
|
||||
const data =
|
||||
let data =
|
||||
pinData ??
|
||||
connectedOutputIndexes
|
||||
.map((outputIndex) =>
|
||||
|
@ -157,9 +166,19 @@ const loadNodeData = async ({ node, connectedOutputIndexes }: SchemaNode) => {
|
|||
)
|
||||
.flat();
|
||||
|
||||
if (data.length === 0 && PREVIEW_SCHEMAS.hasOwnProperty(node.type)) {
|
||||
nodesData.value[node.name] = {
|
||||
schema: PREVIEW_SCHEMAS[node.type],
|
||||
itemsCount: 0,
|
||||
isPreview: true,
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
nodesData.value[node.name] = {
|
||||
schema: getSchemaForExecutionData(data),
|
||||
itemsCount: data.length,
|
||||
isPreview: false,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -316,6 +335,9 @@ watch(
|
|||
<span v-if="nodeAdditionalInfo(currentNode.node)" :class="$style.subtitle">{{
|
||||
nodeAdditionalInfo(currentNode.node)
|
||||
}}</span>
|
||||
<span v-if="currentNode.isPreview" :class="$style.preview">
|
||||
{{ i18n.baseText('dataMapping.schemaView.previewNode') }}
|
||||
</span>
|
||||
</div>
|
||||
<font-awesome-icon
|
||||
v-if="currentNode.nodeType.group.includes('trigger')"
|
||||
|
@ -359,6 +381,16 @@ watch(
|
|||
@transitionstart="(event) => onTransitionStart(event, currentNode.node.name)"
|
||||
>
|
||||
<div :class="$style.innerSchema" @transitionstart.stop>
|
||||
<N8nNotice v-if="currentNode.isPreview" :class="$style.previewNotice" theme="warning">
|
||||
<i18n-t keypath="dataMapping.schemaView.preview">
|
||||
<template #link>
|
||||
<N8nLink :to="DATA_EDITING_DOCS_URL" size="small">
|
||||
{{ i18n.baseText('generic.learnMore') }}
|
||||
</N8nLink>
|
||||
</template>
|
||||
</i18n-t>
|
||||
</N8nNotice>
|
||||
|
||||
<div
|
||||
v-if="currentNode.node.disabled"
|
||||
:class="$style.notice"
|
||||
|
@ -387,6 +419,7 @@ watch(
|
|||
:distance-from-active="currentNode.depth"
|
||||
:node="currentNode.node"
|
||||
:search="search"
|
||||
:preview="currentNode.isPreview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -594,6 +627,10 @@ watch(
|
|||
transform 0.2s $ease-out-expo;
|
||||
}
|
||||
|
||||
.preview {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.triggerIcon {
|
||||
margin-left: var(--spacing-2xs);
|
||||
color: var(--color-primary);
|
||||
|
@ -605,6 +642,11 @@ watch(
|
|||
}
|
||||
}
|
||||
|
||||
.previewNotice {
|
||||
margin-left: var(--spacing-l);
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@container schema (max-width: 24em) {
|
||||
.depth {
|
||||
display: none;
|
||||
|
|
|
@ -18,6 +18,7 @@ type Props = {
|
|||
distanceFromActive?: number;
|
||||
node: INodeUi | null;
|
||||
search: string;
|
||||
preview?: boolean;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
@ -86,6 +87,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
|||
[$style.pill]: true,
|
||||
[$style.mappable]: mappingEnabled,
|
||||
[$style.highlight]: dragged,
|
||||
[$style.preview]: preview,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
|
@ -139,6 +141,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
|||
:distance-from-active="distanceFromActive"
|
||||
:node="node"
|
||||
:search="search"
|
||||
:preview="preview"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -267,6 +270,19 @@ const getIconBySchemaType = (type: Schema['type']): string => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.preview {
|
||||
border-style: dashed;
|
||||
border-width: 1.5px;
|
||||
|
||||
.label {
|
||||
color: var(--color-text-light);
|
||||
}
|
||||
|
||||
.label > span {
|
||||
border-left: 1.5px dashed var(--color-foreground-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
|||
INodeUi,
|
||||
IWorkflowDataCreate,
|
||||
NodeCreatorOpenSource,
|
||||
Schema,
|
||||
} from './Interface';
|
||||
import { NodeConnectionType } from 'n8n-workflow';
|
||||
import type {
|
||||
|
@ -120,6 +121,7 @@ export const FUNCTION_NODE_TYPE = 'n8n-nodes-base.function';
|
|||
export const GITHUB_TRIGGER_NODE_TYPE = 'n8n-nodes-base.githubTrigger';
|
||||
export const GIT_NODE_TYPE = 'n8n-nodes-base.git';
|
||||
export const GOOGLE_GMAIL_NODE_TYPE = 'n8n-nodes-base.gmail';
|
||||
export const GOOGLE_GMAIL_TRIGGER_NODE_TYPE = 'n8n-nodes-base.gmailTrigger';
|
||||
export const GOOGLE_SHEETS_NODE_TYPE = 'n8n-nodes-base.googleSheets';
|
||||
export const ERROR_TRIGGER_NODE_TYPE = 'n8n-nodes-base.errorTrigger';
|
||||
export const ELASTIC_SECURITY_NODE_TYPE = 'n8n-nodes-base.elasticSecurity';
|
||||
|
@ -932,3 +934,154 @@ export const SAMPLE_SUBWORKFLOW_WORKFLOW: IWorkflowDataCreate = {
|
|||
},
|
||||
pinData: {},
|
||||
};
|
||||
|
||||
export const PREVIEW_SCHEMAS: Record<string, Schema> = {
|
||||
[OPEN_AI_NODE_TYPE]: {
|
||||
type: 'object',
|
||||
value: [
|
||||
{
|
||||
type: 'number',
|
||||
key: 'index',
|
||||
value: '',
|
||||
path: '.index',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
key: 'message',
|
||||
value: [
|
||||
{
|
||||
type: 'string',
|
||||
key: 'role',
|
||||
value: '',
|
||||
path: '.message.role',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'content',
|
||||
value: '',
|
||||
path: '.message.content',
|
||||
},
|
||||
{
|
||||
type: 'null',
|
||||
key: 'refusal',
|
||||
value: '',
|
||||
path: 'message.refusal',
|
||||
},
|
||||
],
|
||||
path: '.message',
|
||||
},
|
||||
{
|
||||
type: 'null',
|
||||
key: 'logprobs',
|
||||
value: '',
|
||||
path: '.logprobs',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'finish_reason',
|
||||
value: '',
|
||||
path: '.finish_reason',
|
||||
},
|
||||
],
|
||||
path: '',
|
||||
},
|
||||
[GOOGLE_GMAIL_TRIGGER_NODE_TYPE]: {
|
||||
type: 'object',
|
||||
value: [
|
||||
{
|
||||
type: 'string',
|
||||
key: 'id',
|
||||
value: '',
|
||||
path: '.id',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'threadId',
|
||||
value: '',
|
||||
path: '.threadId',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'snippet',
|
||||
value: '',
|
||||
path: '.snippet',
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
key: 'payload',
|
||||
value: [
|
||||
{
|
||||
type: 'string',
|
||||
key: 'mimeType',
|
||||
value: '',
|
||||
path: 'payload.mimeType',
|
||||
},
|
||||
],
|
||||
path: '.payload',
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'sizeEstimate',
|
||||
value: '',
|
||||
path: '.sizeEstimate',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'historyId',
|
||||
value: '',
|
||||
path: '.historyId',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'internalDate',
|
||||
value: '',
|
||||
path: '.internalDate',
|
||||
},
|
||||
{
|
||||
type: 'array',
|
||||
key: 'labels',
|
||||
value: [
|
||||
{
|
||||
type: 'object',
|
||||
key: '0',
|
||||
value: [
|
||||
{
|
||||
type: 'string',
|
||||
key: 'id',
|
||||
value: '',
|
||||
path: '.labels[0].id',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'name',
|
||||
value: '',
|
||||
path: '.labels[0].name',
|
||||
},
|
||||
],
|
||||
path: '.labels[0]',
|
||||
},
|
||||
],
|
||||
path: '.labels',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'From',
|
||||
value: '',
|
||||
path: '.From',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'To',
|
||||
value: '',
|
||||
path: '.To',
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
key: 'Subject',
|
||||
value: '',
|
||||
path: '.Subject',
|
||||
},
|
||||
],
|
||||
path: '',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -642,6 +642,8 @@
|
|||
"dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty",
|
||||
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
|
||||
"dataMapping.schemaView.noMatches": "No results for '{search}'",
|
||||
"dataMapping.schemaView.preview": "This is a preview of the schema, execute the node to see the exact schema and data. {link}",
|
||||
"dataMapping.schemaView.previewNode": "(schema preview)",
|
||||
"displayWithChange.cancelEdit": "Cancel Edit",
|
||||
"displayWithChange.clickToChange": "Click to Change",
|
||||
"displayWithChange.setValue": "Set Value",
|
||||
|
@ -959,6 +961,8 @@
|
|||
"ndv.input.noOutputData.executePrevious": "Execute previous nodes",
|
||||
"ndv.input.noOutputData.title": "No input data yet",
|
||||
"ndv.input.noOutputData.hint": "(From the earliest node that has no output data yet)",
|
||||
"ndv.input.noOutputData.schemaPreviewHint": "switch to {schema} to use the schema preview",
|
||||
"ndv.input.noOutputData.or": "or",
|
||||
"ndv.input.executingPrevious": "Executing previous nodes...",
|
||||
"ndv.input.notConnected.title": "Wire me up",
|
||||
"ndv.input.notConnected.message": "This node can only receive input data if you connect it to another node.",
|
||||
|
|
Loading…
Reference in a new issue