mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
fix(editor): Fix schema view bugs (#14734)
Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
@@ -190,6 +190,7 @@ describe('Data mapping', () => {
|
|||||||
|
|
||||||
ndv.actions.executePrevious();
|
ndv.actions.executePrevious();
|
||||||
|
|
||||||
|
ndv.getters.schemaViewNode().contains('Schedule').click();
|
||||||
const dataPill = ndv.getters
|
const dataPill = ndv.getters
|
||||||
.inputDataContainer()
|
.inputDataContainer()
|
||||||
.findChildByTestId('run-data-schema-item')
|
.findChildByTestId('run-data-schema-item')
|
||||||
|
|||||||
@@ -150,8 +150,7 @@ export class NDV extends BasePage {
|
|||||||
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
|
nodeRunErrorDescription: () => cy.getByTestId('node-error-description'),
|
||||||
fixedCollectionParameter: (paramName: string) =>
|
fixedCollectionParameter: (paramName: string) =>
|
||||||
cy.getByTestId(`fixed-collection-${paramName}`),
|
cy.getByTestId(`fixed-collection-${paramName}`),
|
||||||
schemaViewNode: () => cy.getByTestId('run-data-schema-node'),
|
schemaViewNode: () => cy.getByTestId('run-data-schema-header'),
|
||||||
schemaViewNodeName: () => cy.getByTestId('run-data-schema-node-name'),
|
|
||||||
expressionExpanders: () => cy.getByTestId('expander'),
|
expressionExpanders: () => cy.getByTestId('expander'),
|
||||||
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
expressionModalOutput: () => cy.getByTestId('expression-modal-output'),
|
||||||
floatingNodes: () => cy.getByTestId('floating-node'),
|
floatingNodes: () => cy.getByTestId('floating-node'),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
import { ref, onMounted, onBeforeUnmount, computed, watch } from 'vue';
|
||||||
import { createEventBus } from '@n8n/utils/event-bus';
|
import { createEventBus } from '@n8n/utils/event-bus';
|
||||||
import type { IRunData, Workflow, NodeConnectionType } from 'n8n-workflow';
|
import type { IRunData, Workflow, NodeConnectionType, IConnectedNode } from 'n8n-workflow';
|
||||||
import { jsonParse, NodeHelpers, NodeConnectionTypes } from 'n8n-workflow';
|
import { jsonParse, NodeHelpers, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
import type { IUpdateInformation, TargetItem } from '@/Interface';
|
import type { IUpdateInformation, TargetItem } from '@/Interface';
|
||||||
|
|
||||||
@@ -130,24 +130,19 @@ const workflowRunData = computed(() => {
|
|||||||
|
|
||||||
const parentNodes = computed(() => {
|
const parentNodes = computed(() => {
|
||||||
if (activeNode.value) {
|
if (activeNode.value) {
|
||||||
return (
|
return props.workflowObject.getParentNodesByDepth(activeNode.value.name, 1);
|
||||||
props.workflowObject
|
|
||||||
.getParentNodesByDepth(activeNode.value.name, 1)
|
|
||||||
.map(({ name }) => name) || []
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const parentNode = computed(() => {
|
const parentNode = computed<IConnectedNode | undefined>(() => {
|
||||||
for (const parentNodeName of parentNodes.value) {
|
for (const parent of parentNodes.value) {
|
||||||
if (workflowsStore?.pinnedWorkflowData?.[parentNodeName]) {
|
if (workflowsStore?.pinnedWorkflowData?.[parent.name]) {
|
||||||
return parentNodeName;
|
return parent;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (workflowRunData.value?.[parentNodeName]) {
|
if (workflowRunData.value?.[parent.name]) {
|
||||||
return parentNodeName;
|
return parent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return parentNodes.value[0];
|
return parentNodes.value[0];
|
||||||
@@ -177,7 +172,7 @@ const inputNodeName = computed<string | undefined>(() => {
|
|||||||
)?.[0];
|
)?.[0];
|
||||||
return connectedOutputNode;
|
return connectedOutputNode;
|
||||||
}
|
}
|
||||||
return selectedInput.value || parentNode.value;
|
return selectedInput.value ?? parentNode.value?.name;
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputNode = computed(() => {
|
const inputNode = computed(() => {
|
||||||
@@ -290,12 +285,23 @@ const maxInputRun = computed(() => {
|
|||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const connectedCurrentNodeOutputs = computed(() => {
|
||||||
|
return parentNodes.value.find(({ name }) => name === inputNodeName.value)?.indicies;
|
||||||
|
});
|
||||||
|
|
||||||
const inputRun = computed(() => {
|
const inputRun = computed(() => {
|
||||||
if (isLinkingEnabled.value && maxOutputRun.value === maxInputRun.value) {
|
if (isLinkingEnabled.value && maxOutputRun.value === maxInputRun.value) {
|
||||||
return outputRun.value;
|
return outputRun.value;
|
||||||
}
|
}
|
||||||
if (runInputIndex.value === -1) {
|
const currentInputNodeName = inputNodeName.value;
|
||||||
return maxInputRun.value;
|
if (runInputIndex.value === -1 && currentInputNodeName) {
|
||||||
|
return (
|
||||||
|
connectedCurrentNodeOutputs.value
|
||||||
|
?.map((outputIndex) =>
|
||||||
|
nodeHelpers.getLastRunIndexWithData(currentInputNodeName, outputIndex),
|
||||||
|
)
|
||||||
|
.find((runIndex) => runIndex !== -1) ?? maxInputRun.value
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.min(runInputIndex.value, maxInputRun.value);
|
return Math.min(runInputIndex.value, maxInputRun.value);
|
||||||
|
|||||||
@@ -262,9 +262,8 @@ const nodeType = computed(() => {
|
|||||||
|
|
||||||
const isSchemaView = computed(() => displayMode.value === 'schema');
|
const isSchemaView = computed(() => displayMode.value === 'schema');
|
||||||
const isSearchInSchemaView = computed(() => isSchemaView.value && !!search.value);
|
const isSearchInSchemaView = computed(() => isSchemaView.value && !!search.value);
|
||||||
const displaysMultipleNodes = computed(
|
const hasMultipleInputNodes = computed(() => props.paneType === 'input' && props.nodes.length > 0);
|
||||||
() => isSchemaView.value && props.paneType === 'input' && props.nodes.length > 0,
|
const displaysMultipleNodes = computed(() => isSchemaView.value && hasMultipleInputNodes.value);
|
||||||
);
|
|
||||||
|
|
||||||
const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type));
|
const isTriggerNode = computed(() => !!node.value && nodeTypesStore.isTriggerNode(node.value.type));
|
||||||
|
|
||||||
@@ -478,7 +477,10 @@ const isPaneTypeOutput = computed(() => props.paneType === 'output');
|
|||||||
|
|
||||||
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
const readOnlyEnv = computed(() => sourceControlStore.preferences.branchReadOnly);
|
||||||
const showIOSearch = computed(
|
const showIOSearch = computed(
|
||||||
() => hasNodeRun.value && !hasRunError.value && unfilteredInputData.value.length > 0,
|
() =>
|
||||||
|
hasNodeRun.value &&
|
||||||
|
!hasRunError.value &&
|
||||||
|
(unfilteredInputData.value.length > 0 || displaysMultipleNodes.value),
|
||||||
);
|
);
|
||||||
const inputSelectLocation = computed(() => {
|
const inputSelectLocation = computed(() => {
|
||||||
if (isSchemaView.value) return 'none';
|
if (isSchemaView.value) return 'none';
|
||||||
@@ -492,7 +494,8 @@ const inputSelectLocation = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const showIoSearchNoMatchContent = computed(
|
const showIoSearchNoMatchContent = computed(
|
||||||
() => hasNodeRun.value && !inputData.value.length && !!search.value,
|
() =>
|
||||||
|
hasNodeRun.value && !inputData.value.length && !!search.value && !displaysMultipleNodes.value,
|
||||||
);
|
);
|
||||||
|
|
||||||
const parentNodeOutputData = computed(() => {
|
const parentNodeOutputData = computed(() => {
|
||||||
@@ -565,7 +568,7 @@ const hasInputOverwrite = computed((): boolean => {
|
|||||||
if (!node.value) {
|
if (!node.value) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const taskData = nodeHelpers.getNodeTaskData(node.value, props.runIndex);
|
const taskData = nodeHelpers.getNodeTaskData(node.value.name, props.runIndex);
|
||||||
return Boolean(taskData?.inputOverride);
|
return Boolean(taskData?.inputOverride);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1395,7 +1398,9 @@ defineExpose({ enterEditMode });
|
|||||||
<RunDataDisplayModeSelect
|
<RunDataDisplayModeSelect
|
||||||
v-show="
|
v-show="
|
||||||
hasPreviewSchema ||
|
hasPreviewSchema ||
|
||||||
(hasNodeRun && (inputData.length || binaryData.length || search) && !editMode.enabled)
|
(hasNodeRun &&
|
||||||
|
(inputData.length || binaryData.length || search || hasMultipleInputNodes) &&
|
||||||
|
!editMode.enabled)
|
||||||
"
|
"
|
||||||
:class="$style.displayModeSelect"
|
:class="$style.displayModeSelect"
|
||||||
:compact="props.compact"
|
:compact="props.compact"
|
||||||
@@ -1679,7 +1684,10 @@ defineExpose({ enterEditMode });
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="
|
v-else-if="
|
||||||
hasNodeRun && (!unfilteredDataCount || (search && !dataCount)) && branches.length > 1
|
hasNodeRun &&
|
||||||
|
(!unfilteredDataCount || (search && !dataCount)) &&
|
||||||
|
!displaysMultipleNodes &&
|
||||||
|
branches.length > 1
|
||||||
"
|
"
|
||||||
:class="$style.center"
|
:class="$style.center"
|
||||||
>
|
>
|
||||||
@@ -1700,7 +1708,10 @@ defineExpose({ enterEditMode });
|
|||||||
</N8nText>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="hasNodeRun && !inputData.length && !search" :class="$style.center">
|
<div
|
||||||
|
v-else-if="hasNodeRun && !inputData.length && !displaysMultipleNodes && !search"
|
||||||
|
:class="$style.center"
|
||||||
|
>
|
||||||
<slot name="no-output-data">xxx</slot>
|
<slot name="no-output-data">xxx</slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -1816,9 +1827,7 @@ defineExpose({ enterEditMode });
|
|||||||
:data="jsonData"
|
:data="jsonData"
|
||||||
:pane-type="paneType"
|
:pane-type="paneType"
|
||||||
:connection-type="connectionType"
|
:connection-type="connectionType"
|
||||||
:run-index="runIndex"
|
|
||||||
:output-index="currentOutputIndex"
|
:output-index="currentOutputIndex"
|
||||||
:total-runs="maxRunIndex"
|
|
||||||
:search="search"
|
:search="search"
|
||||||
:class="$style.schema"
|
:class="$style.schema"
|
||||||
:compact="props.compact"
|
:compact="props.compact"
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ async function setupStore() {
|
|||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
const nodeTypesStore = useNodeTypesStore();
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
settingsStore.setSettings(defaultSettings);
|
settingsStore.setSettings(defaultSettings);
|
||||||
|
|
||||||
nodeTypesStore.setNodeTypes([
|
nodeTypesStore.setNodeTypes([
|
||||||
@@ -124,6 +125,7 @@ async function setupStore() {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
workflowsStore.workflow = workflow as IWorkflowDb;
|
workflowsStore.workflow = workflow as IWorkflowDb;
|
||||||
|
ndvStore.activeNodeName = 'Test Node Name';
|
||||||
|
|
||||||
return pinia;
|
return pinia;
|
||||||
}
|
}
|
||||||
@@ -133,6 +135,8 @@ function mockNodeOutputData(nodeName: string, data: INodeExecutionData[], output
|
|||||||
vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => {
|
vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => {
|
||||||
return {
|
return {
|
||||||
...originalNodeHelpers,
|
...originalNodeHelpers,
|
||||||
|
getLastRunIndexWithData: vi.fn(() => 0),
|
||||||
|
hasNodeExecuted: vi.fn(() => true),
|
||||||
getNodeInputData: vi.fn((node, _, output) => {
|
getNodeInputData: vi.fn((node, _, output) => {
|
||||||
if (node.name === nodeName && output === outputIndex) {
|
if (node.name === nodeName && output === outputIndex) {
|
||||||
return data;
|
return data;
|
||||||
@@ -168,6 +172,8 @@ describe('VirtualSchema.vue', () => {
|
|||||||
cleanup();
|
cleanup();
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
vi.setSystemTime('2025-01-01');
|
vi.setSystemTime('2025-01-01');
|
||||||
|
const pinia = await setupStore();
|
||||||
|
|
||||||
renderComponent = createComponentRenderer(VirtualSchema, {
|
renderComponent = createComponentRenderer(VirtualSchema, {
|
||||||
global: {
|
global: {
|
||||||
stubs: {
|
stubs: {
|
||||||
@@ -177,12 +183,9 @@ describe('VirtualSchema.vue', () => {
|
|||||||
Notice: NoticeStub,
|
Notice: NoticeStub,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
pinia: await setupStore(),
|
pinia,
|
||||||
props: {
|
props: {
|
||||||
mappingEnabled: true,
|
mappingEnabled: true,
|
||||||
runIndex: 1,
|
|
||||||
outputIndex: 0,
|
|
||||||
totalRuns: 2,
|
|
||||||
paneType: 'input',
|
paneType: 'input',
|
||||||
connectionType: 'main',
|
connectionType: 'main',
|
||||||
search: '',
|
search: '',
|
||||||
@@ -192,13 +195,9 @@ describe('VirtualSchema.vue', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('renders schema for empty data for unexecuted nodes', async () => {
|
it('renders schema for empty data for unexecuted nodes', async () => {
|
||||||
const { getAllByText, getAllByTestId } = renderComponent();
|
const { getAllByText } = renderComponent();
|
||||||
|
|
||||||
await waitFor(() => expect(getAllByText('Execute previous nodes').length).toBe(2));
|
await waitFor(() => expect(getAllByText('Execute previous nodes').length).toBe(1));
|
||||||
|
|
||||||
// Collapse second node
|
|
||||||
await userEvent.click(getAllByTestId('run-data-schema-header')[1]);
|
|
||||||
expect(getAllByText('Execute previous nodes').length).toBe(1);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders schema for empty data with binary', async () => {
|
it('renders schema for empty data with binary', async () => {
|
||||||
@@ -320,7 +319,9 @@ describe('VirtualSchema.vue', () => {
|
|||||||
|
|
||||||
const { getAllByText } = renderComponent({ props: { paneType: 'output' } });
|
const { getAllByText } = renderComponent({ props: { paneType: 'output' } });
|
||||||
await waitFor(() =>
|
await waitFor(() =>
|
||||||
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1),
|
expect(
|
||||||
|
getAllByText('No fields - node executed, but no items were sent on this branch').length,
|
||||||
|
).toBe(1),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -352,7 +353,6 @@ describe('VirtualSchema.vue', () => {
|
|||||||
const headers = getAllByTestId('run-data-schema-header');
|
const headers = getAllByTestId('run-data-schema-header');
|
||||||
expect(headers[0]).toHaveTextContent('If');
|
expect(headers[0]).toHaveTextContent('If');
|
||||||
expect(headers[0]).toHaveTextContent('2 items');
|
expect(headers[0]).toHaveTextContent('2 items');
|
||||||
expect(headers[0]).toMatchSnapshot();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -434,7 +434,10 @@ describe('VirtualSchema.vue', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => expect(getAllByTestId('run-data-schema-item').length).toBe(2));
|
await waitFor(() => {
|
||||||
|
expect(getAllByTestId('run-data-schema-header')).toHaveLength(3);
|
||||||
|
expect(getAllByTestId('run-data-schema-item').length).toBe(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show connections', async () => {
|
it('should show connections', async () => {
|
||||||
@@ -564,30 +567,27 @@ describe('VirtualSchema.vue', () => {
|
|||||||
data: [{ json: { name: 'John' } }],
|
data: [{ json: { name: 'John' } }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getAllByTestId, queryAllByTestId, rerender } = renderComponent();
|
const { getAllByTestId, queryAllByTestId, rerender, container } = renderComponent();
|
||||||
|
|
||||||
let headers: HTMLElement[] = [];
|
let headers: HTMLElement[] = [];
|
||||||
await waitFor(async () => {
|
await waitFor(async () => {
|
||||||
headers = getAllByTestId('run-data-schema-header');
|
headers = getAllByTestId('run-data-schema-header');
|
||||||
expect(headers.length).toBe(3);
|
expect(headers.length).toBe(3);
|
||||||
expect(getAllByTestId('run-data-schema-item').length).toBe(2);
|
expect(getAllByTestId('run-data-schema-item').length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Collapse all nodes (Variables & context is collapsed by default)
|
// Collapse first node (expanded by default)
|
||||||
await Promise.all(headers.slice(0, -1).map(async (header) => await userEvent.click(header)));
|
await userEvent.click(headers[0]);
|
||||||
|
|
||||||
expect(queryAllByTestId('run-data-schema-item').length).toBe(0);
|
expect(queryAllByTestId('run-data-schema-item').length).toBe(0);
|
||||||
|
|
||||||
await rerender({ search: 'John' });
|
await rerender({ search: 'John' });
|
||||||
|
|
||||||
expect(getAllByTestId('run-data-schema-item').length).toBe(13);
|
expect(getAllByTestId('run-data-schema-item').length).toBe(2);
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders preview schema when enabled and available', async () => {
|
it('renders preview schema when enabled and available', async () => {
|
||||||
useWorkflowsStore().pinData({
|
|
||||||
node: mockNode1,
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
useWorkflowsStore().pinData({
|
useWorkflowsStore().pinData({
|
||||||
node: mockNode2,
|
node: mockNode2,
|
||||||
data: [],
|
data: [],
|
||||||
@@ -611,14 +611,18 @@ describe('VirtualSchema.vue', () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getAllByTestId, queryAllByText, container } = renderComponent({});
|
const { getAllByTestId, queryAllByText, container } = renderComponent({
|
||||||
|
props: {
|
||||||
await waitFor(() => {
|
nodes: [{ name: mockNode2.name, indicies: [], depth: 1 }],
|
||||||
expect(getAllByTestId('run-data-schema-header')).toHaveLength(3);
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getAllByTestId('run-data-schema-header')).toHaveLength(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getAllByTestId('schema-preview-warning')).toHaveLength(1);
|
||||||
expect(queryAllByText("No fields - item(s) exist, but they're empty")).toHaveLength(0);
|
expect(queryAllByText("No fields - item(s) exist, but they're empty")).toHaveLength(0);
|
||||||
expect(getAllByTestId('schema-preview-warning')).toHaveLength(2);
|
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -56,9 +56,6 @@ type Props = {
|
|||||||
node?: INodeUi | null;
|
node?: INodeUi | null;
|
||||||
data?: IDataObject[];
|
data?: IDataObject[];
|
||||||
mappingEnabled?: boolean;
|
mappingEnabled?: boolean;
|
||||||
runIndex?: number;
|
|
||||||
outputIndex?: number;
|
|
||||||
totalRuns?: number;
|
|
||||||
paneType: 'input' | 'output';
|
paneType: 'input' | 'output';
|
||||||
connectionType?: NodeConnectionType;
|
connectionType?: NodeConnectionType;
|
||||||
search?: string;
|
search?: string;
|
||||||
@@ -70,9 +67,6 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
distanceFromActive: 1,
|
distanceFromActive: 1,
|
||||||
node: null,
|
node: null,
|
||||||
data: () => [],
|
data: () => [],
|
||||||
runIndex: 0,
|
|
||||||
outputIndex: 0,
|
|
||||||
totalRuns: 1,
|
|
||||||
connectionType: NodeConnectionTypes.Main,
|
connectionType: NodeConnectionTypes.Main,
|
||||||
search: '',
|
search: '',
|
||||||
mappingEnabled: false,
|
mappingEnabled: false,
|
||||||
@@ -91,20 +85,24 @@ const posthogStore = usePostHog();
|
|||||||
|
|
||||||
const { getSchemaForExecutionData, getSchemaForJsonSchema, getSchema, filterSchema } =
|
const { getSchemaForExecutionData, getSchemaForJsonSchema, getSchema, filterSchema } =
|
||||||
useDataSchema();
|
useDataSchema();
|
||||||
const { closedNodes, flattenSchema, flattenMultipleSchemas, toggleLeaf, toggleNode } =
|
const { closedNodes, flattenSchema, flattenMultipleSchemas, toggleNode } = useFlattenSchema();
|
||||||
useFlattenSchema();
|
const { getNodeInputData, getLastRunIndexWithData, hasNodeExecuted } = useNodeHelpers();
|
||||||
const { getNodeInputData, getNodeTaskData } = useNodeHelpers();
|
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
'clear:search': [];
|
'clear:search': [];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const scroller = ref<RecycleScrollerInstance>();
|
const scroller = ref<RecycleScrollerInstance>();
|
||||||
|
const closedNodesBeforeSearch = ref(new Set<string>());
|
||||||
|
|
||||||
const canDraggableDrop = computed(() => ndvStore.canDraggableDrop);
|
const canDraggableDrop = computed(() => ndvStore.canDraggableDrop);
|
||||||
const draggableStickyPosition = computed(() => ndvStore.draggableStickyPos);
|
const draggableStickyPosition = computed(() => ndvStore.draggableStickyPos);
|
||||||
|
|
||||||
const toggleNodeAndScrollTop = (id: string) => {
|
const toggleNodeExclusiveAndScrollTop = (id: string) => {
|
||||||
|
const isClosed = closedNodes.value.has(id);
|
||||||
|
if (isClosed) {
|
||||||
|
closedNodes.value = new Set(items.value.map((item) => item.id));
|
||||||
|
}
|
||||||
toggleNode(id);
|
toggleNode(id);
|
||||||
scroller.value?.scrollToItem(0);
|
scroller.value?.scrollToItem(0);
|
||||||
};
|
};
|
||||||
@@ -112,13 +110,23 @@ const toggleNodeAndScrollTop = (id: string) => {
|
|||||||
const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) => {
|
const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) => {
|
||||||
const pinData = workflowsStore.pinDataByNodeName(connectedNode.name);
|
const pinData = workflowsStore.pinDataByNodeName(connectedNode.name);
|
||||||
const hasPinnedData = pinData ? pinData.length > 0 : false;
|
const hasPinnedData = pinData ? pinData.length > 0 : false;
|
||||||
const isNodeExecuted = getNodeTaskData(fullNode, props.runIndex) !== null || hasPinnedData;
|
const isNodeExecuted = hasPinnedData || hasNodeExecuted(connectedNode.name);
|
||||||
|
|
||||||
const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0];
|
const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0];
|
||||||
const nodeData = connectedOutputIndexes.map((outputIndex) =>
|
const connectedOutputsWithData = connectedOutputIndexes
|
||||||
getNodeInputData(fullNode, props.runIndex, outputIndex, props.paneType, props.connectionType),
|
.map((outputIndex) => ({
|
||||||
);
|
outputIndex,
|
||||||
const hasBinary = nodeData.flat().some((data) => !isEmpty(data.binary));
|
runIndex: getLastRunIndexWithData(fullNode.name, outputIndex, props.connectionType),
|
||||||
const data = pinData ?? nodeData.map(executionDataToJson).flat();
|
}))
|
||||||
|
.filter(({ runIndex }) => runIndex !== -1);
|
||||||
|
const nodeData = connectedOutputsWithData
|
||||||
|
.map(({ outputIndex, runIndex }) =>
|
||||||
|
getNodeInputData(fullNode, runIndex, outputIndex, props.paneType, props.connectionType),
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
const hasBinary = nodeData.some((data) => !isEmpty(data.binary));
|
||||||
|
const data = pinData ?? executionDataToJson(nodeData);
|
||||||
|
const isDataEmpty = data.length === 0;
|
||||||
|
|
||||||
let schema = getSchemaForExecutionData(data);
|
let schema = getSchemaForExecutionData(data);
|
||||||
let preview = false;
|
let preview = false;
|
||||||
@@ -135,9 +143,11 @@ const getNodeSchema = async (fullNode: INodeUi, connectedNode: IConnectedNode) =
|
|||||||
schema,
|
schema,
|
||||||
connectedOutputIndexes,
|
connectedOutputIndexes,
|
||||||
itemsCount: data.length,
|
itemsCount: data.length,
|
||||||
|
runIndex: connectedOutputsWithData[0]?.runIndex ?? 0,
|
||||||
preview,
|
preview,
|
||||||
hasBinary,
|
hasBinary,
|
||||||
isNodeExecuted,
|
isNodeExecuted,
|
||||||
|
isDataEmpty,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -164,7 +174,7 @@ const contextSchema = computed(() => {
|
|||||||
$workflow: pick(workflowsStore.workflow, ['id', 'name', 'active']),
|
$workflow: pick(workflowsStore.workflow, ['id', 'name', 'active']),
|
||||||
};
|
};
|
||||||
|
|
||||||
return getSchema(schemaSource);
|
return filterSchema(getSchema(schemaSource), props.search);
|
||||||
});
|
});
|
||||||
|
|
||||||
const contextItems = computed(() => {
|
const contextItems = computed(() => {
|
||||||
@@ -178,10 +188,13 @@ const contextItems = computed(() => {
|
|||||||
|
|
||||||
if (closedNodes.value.has(header.id)) return [header];
|
if (closedNodes.value.has(header.id)) return [header];
|
||||||
|
|
||||||
const fields: Renders[] = flattenSchema({
|
const schema = contextSchema.value;
|
||||||
schema: contextSchema.value,
|
|
||||||
depth: 1,
|
if (!schema) {
|
||||||
}).flatMap((renderItem) => {
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields: Renders[] = flattenSchema({ schema, depth: 1 }).flatMap((renderItem) => {
|
||||||
const isVars =
|
const isVars =
|
||||||
renderItem.type === 'item' && renderItem.depth === 1 && renderItem.title === '$vars';
|
renderItem.type === 'item' && renderItem.depth === 1 && renderItem.title === '$vars';
|
||||||
|
|
||||||
@@ -252,8 +265,16 @@ const nodesSchemas = asyncComputed<SchemaNode[]>(async () => {
|
|||||||
const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion);
|
const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion);
|
||||||
if (!nodeType) continue;
|
if (!nodeType) continue;
|
||||||
|
|
||||||
const { schema, connectedOutputIndexes, itemsCount, preview, hasBinary, isNodeExecuted } =
|
const {
|
||||||
await getNodeSchema(fullNode, node);
|
schema,
|
||||||
|
connectedOutputIndexes,
|
||||||
|
itemsCount,
|
||||||
|
preview,
|
||||||
|
hasBinary,
|
||||||
|
isNodeExecuted,
|
||||||
|
isDataEmpty,
|
||||||
|
runIndex,
|
||||||
|
} = await getNodeSchema(fullNode, node);
|
||||||
|
|
||||||
const filteredSchema = filterSchema(schema, search);
|
const filteredSchema = filterSchema(schema, search);
|
||||||
|
|
||||||
@@ -269,6 +290,8 @@ const nodesSchemas = asyncComputed<SchemaNode[]>(async () => {
|
|||||||
preview,
|
preview,
|
||||||
hasBinary,
|
hasBinary,
|
||||||
isNodeExecuted,
|
isNodeExecuted,
|
||||||
|
isDataEmpty,
|
||||||
|
runIndex,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -319,25 +342,28 @@ const noSearchResults = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.search,
|
() => Boolean(props.search),
|
||||||
(newSearch) => {
|
(hasSearch) => {
|
||||||
if (!newSearch) return;
|
if (hasSearch) {
|
||||||
closedNodes.value.clear();
|
closedNodesBeforeSearch.value = new Set(closedNodes.value);
|
||||||
|
closedNodes.value.clear();
|
||||||
|
} else if (closedNodes.value.size === 0) {
|
||||||
|
closedNodes.value = closedNodesBeforeSearch.value;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// Variables & context items should be collapsed by default
|
// Collapse all nodes except the first
|
||||||
watch(
|
const unwatchItems = watch(items, (newItems) => {
|
||||||
contextItems,
|
if (newItems.length < 2) return;
|
||||||
(currentContextItems) => {
|
closedNodes.value = new Set(
|
||||||
currentContextItems
|
newItems
|
||||||
.filter((item) => item.type === 'header')
|
.filter((item) => item.type === 'header')
|
||||||
.forEach((item) => {
|
.slice(1)
|
||||||
closedNodes.value.add(item.id);
|
.map((item) => item.id),
|
||||||
});
|
);
|
||||||
},
|
unwatchItems();
|
||||||
{ once: true, immediate: true },
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const onDragStart = (el: HTMLElement, data?: string) => {
|
const onDragStart = (el: HTMLElement, data?: string) => {
|
||||||
ndvStore.draggableStartDragging({
|
ndvStore.draggableStartDragging({
|
||||||
@@ -356,13 +382,14 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
|
|
||||||
const isPreview = parentNode?.preview ?? false;
|
const isPreview = parentNode?.preview ?? false;
|
||||||
const hasCredential = !isEmpty(parentNode?.node.credentials);
|
const hasCredential = !isEmpty(parentNode?.node.credentials);
|
||||||
|
const runIndex = Number(el.dataset.runIndex);
|
||||||
|
|
||||||
const telemetryPayload = {
|
const telemetryPayload = {
|
||||||
src_node_type: el.dataset.nodeType,
|
src_node_type: el.dataset.nodeType,
|
||||||
src_field_name: el.dataset.name ?? '',
|
src_field_name: el.dataset.name ?? '',
|
||||||
src_nodes_back: el.dataset.depth,
|
src_nodes_back: el.dataset.depth,
|
||||||
src_run_index: props.runIndex,
|
src_run_index: runIndex,
|
||||||
src_runs_total: props.totalRuns,
|
src_runs_total: runIndex,
|
||||||
src_field_nest_level: el.dataset.level ?? 0,
|
src_field_nest_level: el.dataset.level ?? 0,
|
||||||
src_view: isPreview ? 'schema_preview' : 'schema',
|
src_view: isPreview ? 'schema_preview' : 'schema',
|
||||||
src_has_credential: hasCredential,
|
src_has_credential: hasCredential,
|
||||||
@@ -424,8 +451,8 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
v-if="item.type === 'header'"
|
v-if="item.type === 'header'"
|
||||||
v-bind="item"
|
v-bind="item"
|
||||||
:collapsed="closedNodes.has(item.id)"
|
:collapsed="closedNodes.has(item.id)"
|
||||||
@click:toggle="toggleLeaf(item.id)"
|
@click:toggle="toggleNode(item.id)"
|
||||||
@click="toggleNodeAndScrollTop(item.id)"
|
@click="toggleNodeExclusiveAndScrollTop(item.id)"
|
||||||
/>
|
/>
|
||||||
<VirtualSchemaItem
|
<VirtualSchemaItem
|
||||||
v-else-if="item.type === 'item'"
|
v-else-if="item.type === 'item'"
|
||||||
@@ -434,7 +461,7 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
:draggable="mappingEnabled"
|
:draggable="mappingEnabled"
|
||||||
:collapsed="closedNodes.has(item.id)"
|
:collapsed="closedNodes.has(item.id)"
|
||||||
:highlight="ndvStore.highlightDraggables"
|
:highlight="ndvStore.highlightDraggables"
|
||||||
@click="toggleLeaf(item.id)"
|
@click="toggleNode(item.id)"
|
||||||
>
|
>
|
||||||
</VirtualSchemaItem>
|
</VirtualSchemaItem>
|
||||||
|
|
||||||
@@ -461,13 +488,14 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
>
|
>
|
||||||
<template #link>
|
<template #link>
|
||||||
<NodeExecuteButton
|
<NodeExecuteButton
|
||||||
:node-name="item.nodeName"
|
v-if="ndvStore.activeNodeName"
|
||||||
|
:node-name="ndvStore.activeNodeName"
|
||||||
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
|
:label="i18n.baseText('ndv.input.noOutputData.executePrevious')"
|
||||||
text
|
text
|
||||||
telemetry-source="inputs"
|
telemetry-source="inputs"
|
||||||
hide-icon
|
hide-icon
|
||||||
size="small"
|
size="small"
|
||||||
class="execute-button"
|
:class="$style.executeButton"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
@@ -481,6 +509,12 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" module>
|
||||||
|
.executeButton {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.full-height {
|
.full-height {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -532,8 +566,4 @@ const onDragEnd = (el: HTMLElement) => {
|
|||||||
padding-bottom: var(--spacing-xs);
|
padding-bottom: var(--spacing-xs);
|
||||||
margin-left: calc((var(--spacing-xl) * var(--schema-level)));
|
margin-left: calc((var(--spacing-xl) * var(--schema-level)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.execute-button {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ type Props = {
|
|||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
lockedTooltip?: string;
|
lockedTooltip?: string;
|
||||||
|
runIndex?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const props = defineProps<Props>();
|
const props = defineProps<Props>();
|
||||||
@@ -48,6 +49,7 @@ const emit = defineEmits<{
|
|||||||
:data-node-type="nodeType"
|
:data-node-type="nodeType"
|
||||||
:data-target="!locked && 'mappable'"
|
:data-target="!locked && 'mappable'"
|
||||||
:data-node-name="nodeName"
|
:data-node-name="nodeName"
|
||||||
|
:data-run-index="runIndex"
|
||||||
class="pill"
|
class="pill"
|
||||||
:class="{
|
:class="{
|
||||||
'pill--highlight': highlight,
|
'pill--highlight': highlight,
|
||||||
|
|||||||
@@ -21,357 +21,6 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="schema-header-wrapper"
|
|
||||||
data-v-882a318e=""
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
id="Manual Trigger"
|
|
||||||
type="header"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="schema-header"
|
|
||||||
data-test-id="run-data-schema-header"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="toggle"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="collapse-icon"
|
|
||||||
data-v-882a318e=""
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="angle-down"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="n8n-node-icon icon icon-trigger icon icon-trigger"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="nodeIconWrapper"
|
|
||||||
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
|
|
||||||
>
|
|
||||||
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="icon"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="nodeIconImage"
|
|
||||||
src="/nodes/test-node/icon.svg"
|
|
||||||
/>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="title"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
Manual Trigger
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="trigger-icon"
|
|
||||||
data-v-882a318e=""
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="bolt"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
size="xs"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="extra-info"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
Preview
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="notice"
|
|
||||||
data-test-id="schema-preview-warning"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
|
|
||||||
Usually outputs the following fields. Execute the node to see the actual ones.
|
|
||||||
|
|
||||||
<a
|
|
||||||
class="n8n-link"
|
|
||||||
data-v-882a318e=""
|
|
||||||
href="https://docs.n8n.io/data/schema-preview/"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="primary"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="n8n-text size-small bold"
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
Learn more
|
|
||||||
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</a>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="schema-item draggable"
|
|
||||||
data-test-id="run-data-schema-item"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
type="item"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="toggle-container"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="toggle"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="collapse-icon"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="angle-down"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="pill pill--preview"
|
|
||||||
data-depth="1"
|
|
||||||
data-name="account"
|
|
||||||
data-nest-level="1"
|
|
||||||
data-node-name="Manual Trigger"
|
|
||||||
data-node-type="n8n-nodes-base.manualTrigger"
|
|
||||||
data-path=".account"
|
|
||||||
data-target="mappable"
|
|
||||||
data-test-id="run-data-schema-node-name"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
data-value="{{ $json.account }}"
|
|
||||||
>
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="type-icon"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="cube"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
size="sm"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="content title"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<!--v-if-->
|
|
||||||
account
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
<span
|
|
||||||
class="content text"
|
|
||||||
data-test-id="run-data-schema-item-value"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
<span />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="schema-item draggable"
|
|
||||||
data-test-id="run-data-schema-item"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
type="item"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="toggle-container"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="pill pill--preview"
|
|
||||||
data-depth="1"
|
|
||||||
data-name="id"
|
|
||||||
data-nest-level="2"
|
|
||||||
data-node-name="Manual Trigger"
|
|
||||||
data-node-type="n8n-nodes-base.manualTrigger"
|
|
||||||
data-path=".account.id"
|
|
||||||
data-target="mappable"
|
|
||||||
data-test-id="run-data-schema-node-name"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
data-value="{{ $json.account.id }}"
|
|
||||||
>
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="type-icon"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="font"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
size="sm"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
class="content title"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<!--v-if-->
|
|
||||||
id
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
<span
|
|
||||||
class="content text"
|
|
||||||
data-test-id="run-data-schema-item-value"
|
|
||||||
data-v-0f5e7239=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<span>
|
|
||||||
<!--v-if-->
|
|
||||||
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<span
|
|
||||||
class="n8n-text compact size-14 regular n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger n8n-icon icon el-tooltip__trigger el-tooltip__trigger icon el-tooltip__trigger el-tooltip__trigger"
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="14"
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="ellipsis-h"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</span>
|
|
||||||
<!--teleport start-->
|
|
||||||
<!--teleport end-->
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="schema-header-wrapper"
|
class="schema-header-wrapper"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
@@ -527,7 +176,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="pill pill--preview"
|
class="pill pill--preview"
|
||||||
data-depth="2"
|
data-depth="1"
|
||||||
data-name="account"
|
data-name="account"
|
||||||
data-nest-level="1"
|
data-nest-level="1"
|
||||||
data-node-name="Set2"
|
data-node-name="Set2"
|
||||||
@@ -536,7 +185,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
|||||||
data-target="mappable"
|
data-target="mappable"
|
||||||
data-test-id="run-data-schema-node-name"
|
data-test-id="run-data-schema-node-name"
|
||||||
data-v-0f5e7239=""
|
data-v-0f5e7239=""
|
||||||
data-value="{{ $('Set2').item.json.account }}"
|
data-value="{{ $json.account }}"
|
||||||
>
|
>
|
||||||
<font-awesome-icon-stub
|
<font-awesome-icon-stub
|
||||||
beat="false"
|
beat="false"
|
||||||
@@ -601,7 +250,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="pill pill--preview"
|
class="pill pill--preview"
|
||||||
data-depth="2"
|
data-depth="1"
|
||||||
data-name="id"
|
data-name="id"
|
||||||
data-nest-level="2"
|
data-nest-level="2"
|
||||||
data-node-name="Set2"
|
data-node-name="Set2"
|
||||||
@@ -610,7 +259,7 @@ exports[`VirtualSchema.vue > renders preview schema when enabled and available 1
|
|||||||
data-target="mappable"
|
data-target="mappable"
|
||||||
data-test-id="run-data-schema-node-name"
|
data-test-id="run-data-schema-node-name"
|
||||||
data-v-0f5e7239=""
|
data-v-0f5e7239=""
|
||||||
data-value="{{ $('Set2').item.json.account.id }}"
|
data-value="{{ $json.account.id }}"
|
||||||
>
|
>
|
||||||
<font-awesome-icon-stub
|
<font-awesome-icon-stub
|
||||||
beat="false"
|
beat="false"
|
||||||
@@ -838,79 +487,6 @@ exports[`VirtualSchema.vue > renders previous nodes schema for AI tools 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`VirtualSchema.vue > renders schema for correct output branch 1`] = `
|
|
||||||
<div
|
|
||||||
class="schema-header"
|
|
||||||
data-test-id="run-data-schema-header"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="toggle"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
<font-awesome-icon-stub
|
|
||||||
beat="false"
|
|
||||||
beatfade="false"
|
|
||||||
border="false"
|
|
||||||
bounce="false"
|
|
||||||
class="collapse-icon"
|
|
||||||
data-v-882a318e=""
|
|
||||||
fade="false"
|
|
||||||
fixedwidth="false"
|
|
||||||
flash="false"
|
|
||||||
flip="false"
|
|
||||||
icon="angle-down"
|
|
||||||
inverse="false"
|
|
||||||
listitem="false"
|
|
||||||
pulse="false"
|
|
||||||
shake="false"
|
|
||||||
spin="false"
|
|
||||||
spinpulse="false"
|
|
||||||
spinreverse="false"
|
|
||||||
swapopacity="false"
|
|
||||||
symbol="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="n8n-node-icon icon icon"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="nodeIconWrapper"
|
|
||||||
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
|
|
||||||
>
|
|
||||||
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="icon"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
class="nodeIconImage"
|
|
||||||
src="/nodes/test-node/icon.svg"
|
|
||||||
/>
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="title"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
If
|
|
||||||
<!--v-if-->
|
|
||||||
</div>
|
|
||||||
<!--v-if-->
|
|
||||||
<div
|
|
||||||
class="extra-info"
|
|
||||||
data-test-id="run-data-schema-node-item-count"
|
|
||||||
data-v-882a318e=""
|
|
||||||
>
|
|
||||||
2 items
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`VirtualSchema.vue > renders schema for empty objects and arrays 1`] = `
|
exports[`VirtualSchema.vue > renders schema for empty objects and arrays 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<div
|
<div
|
||||||
@@ -2741,7 +2317,7 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
|
|||||||
beatfade="false"
|
beatfade="false"
|
||||||
border="false"
|
border="false"
|
||||||
bounce="false"
|
bounce="false"
|
||||||
class="collapse-icon"
|
class="collapse-icon collapsed"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
fade="false"
|
fade="false"
|
||||||
fixedwidth="false"
|
fixedwidth="false"
|
||||||
@@ -2797,45 +2373,6 @@ exports[`VirtualSchema.vue > renders schema with spaces and dots 1`] = `
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="empty-schema"
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
style="--schema-level: 1;"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="n8n-text size-small regular"
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
>
|
|
||||||
|
|
||||||
<span
|
|
||||||
data-v-d00cba9a=""
|
|
||||||
>
|
|
||||||
|
|
||||||
|
|
||||||
<button
|
|
||||||
aria-live="polite"
|
|
||||||
class="button button primary small text execute-button el-tooltip__trigger el-tooltip__trigger execute-button el-tooltip__trigger el-tooltip__trigger"
|
|
||||||
title="Runs the current node. Will also run previous nodes if they have not been run yet"
|
|
||||||
transparent-background="false"
|
|
||||||
>
|
|
||||||
<!--v-if-->
|
|
||||||
<span>
|
|
||||||
Execute previous nodes
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
<!--teleport start-->
|
|
||||||
<!--teleport end-->
|
|
||||||
|
|
||||||
|
|
||||||
to see schema
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="schema-header-wrapper"
|
class="schema-header-wrapper"
|
||||||
data-v-882a318e=""
|
data-v-882a318e=""
|
||||||
@@ -3990,6 +3527,397 @@ exports[`VirtualSchema.vue > renders variables and context section 1`] = `
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--teleport start-->
|
||||||
|
<!--teleport end-->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`VirtualSchema.vue > should expand all nodes when searching 1`] = `
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="run-data-schema full-height"
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<div
|
||||||
|
class="full-height"
|
||||||
|
data-test-id="draggable"
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="full-height scroller"
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
min-item-size="38"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="schema-header-wrapper"
|
||||||
|
data-v-882a318e=""
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
id="Manual Trigger"
|
||||||
|
type="header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="schema-header"
|
||||||
|
data-test-id="run-data-schema-header"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="toggle"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
<font-awesome-icon-stub
|
||||||
|
beat="false"
|
||||||
|
beatfade="false"
|
||||||
|
border="false"
|
||||||
|
bounce="false"
|
||||||
|
class="collapse-icon"
|
||||||
|
data-v-882a318e=""
|
||||||
|
fade="false"
|
||||||
|
fixedwidth="false"
|
||||||
|
flash="false"
|
||||||
|
flip="false"
|
||||||
|
icon="angle-down"
|
||||||
|
inverse="false"
|
||||||
|
listitem="false"
|
||||||
|
pulse="false"
|
||||||
|
shake="false"
|
||||||
|
spin="false"
|
||||||
|
spinpulse="false"
|
||||||
|
spinreverse="false"
|
||||||
|
swapopacity="false"
|
||||||
|
symbol="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="n8n-node-icon icon icon-trigger icon icon-trigger"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="nodeIconWrapper"
|
||||||
|
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
|
||||||
|
>
|
||||||
|
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="nodeIconImage"
|
||||||
|
src="/nodes/test-node/icon.svg"
|
||||||
|
/>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="title"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
Manual Trigger
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<font-awesome-icon-stub
|
||||||
|
beat="false"
|
||||||
|
beatfade="false"
|
||||||
|
border="false"
|
||||||
|
bounce="false"
|
||||||
|
class="trigger-icon"
|
||||||
|
data-v-882a318e=""
|
||||||
|
fade="false"
|
||||||
|
fixedwidth="false"
|
||||||
|
flash="false"
|
||||||
|
flip="false"
|
||||||
|
icon="bolt"
|
||||||
|
inverse="false"
|
||||||
|
listitem="false"
|
||||||
|
pulse="false"
|
||||||
|
shake="false"
|
||||||
|
size="xs"
|
||||||
|
spin="false"
|
||||||
|
spinpulse="false"
|
||||||
|
spinreverse="false"
|
||||||
|
swapopacity="false"
|
||||||
|
symbol="false"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="extra-info"
|
||||||
|
data-test-id="run-data-schema-node-item-count"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
1 item
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="schema-item draggable"
|
||||||
|
data-test-id="run-data-schema-item"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
type="item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="toggle-container"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pill"
|
||||||
|
data-depth="1"
|
||||||
|
data-name="name"
|
||||||
|
data-nest-level="1"
|
||||||
|
data-node-name="Manual Trigger"
|
||||||
|
data-node-type="n8n-nodes-base.manualTrigger"
|
||||||
|
data-path=".name"
|
||||||
|
data-target="mappable"
|
||||||
|
data-test-id="run-data-schema-node-name"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
data-value="{{ $json.name }}"
|
||||||
|
>
|
||||||
|
<font-awesome-icon-stub
|
||||||
|
beat="false"
|
||||||
|
beatfade="false"
|
||||||
|
border="false"
|
||||||
|
bounce="false"
|
||||||
|
class="type-icon"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
fade="false"
|
||||||
|
fixedwidth="false"
|
||||||
|
flash="false"
|
||||||
|
flip="false"
|
||||||
|
icon="font"
|
||||||
|
inverse="false"
|
||||||
|
listitem="false"
|
||||||
|
pulse="false"
|
||||||
|
shake="false"
|
||||||
|
size="sm"
|
||||||
|
spin="false"
|
||||||
|
spinpulse="false"
|
||||||
|
spinreverse="false"
|
||||||
|
swapopacity="false"
|
||||||
|
symbol="false"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="title"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<span>
|
||||||
|
name
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
<span
|
||||||
|
class="text"
|
||||||
|
data-test-id="run-data-schema-item-value"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
|
<mark>
|
||||||
|
John
|
||||||
|
</mark>
|
||||||
|
|
||||||
|
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="schema-header-wrapper"
|
||||||
|
data-v-882a318e=""
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
id="Set2"
|
||||||
|
type="header"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="schema-header"
|
||||||
|
data-test-id="run-data-schema-header"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="toggle"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
<font-awesome-icon-stub
|
||||||
|
beat="false"
|
||||||
|
beatfade="false"
|
||||||
|
border="false"
|
||||||
|
bounce="false"
|
||||||
|
class="collapse-icon"
|
||||||
|
data-v-882a318e=""
|
||||||
|
fade="false"
|
||||||
|
fixedwidth="false"
|
||||||
|
flash="false"
|
||||||
|
flip="false"
|
||||||
|
icon="angle-down"
|
||||||
|
inverse="false"
|
||||||
|
listitem="false"
|
||||||
|
pulse="false"
|
||||||
|
shake="false"
|
||||||
|
spin="false"
|
||||||
|
spinpulse="false"
|
||||||
|
spinreverse="false"
|
||||||
|
swapopacity="false"
|
||||||
|
symbol="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="n8n-node-icon icon icon"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="nodeIconWrapper"
|
||||||
|
style="width: 12px; height: 12px; font-size: 12px; line-height: 12px;"
|
||||||
|
>
|
||||||
|
<!-- ElementUI tooltip is prone to memory-leaking so we only render it if we really need it -->
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="icon"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="nodeIconImage"
|
||||||
|
src="/nodes/test-node/icon.svg"
|
||||||
|
/>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="title"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
Set2
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
<div
|
||||||
|
class="extra-info"
|
||||||
|
data-test-id="run-data-schema-node-item-count"
|
||||||
|
data-v-882a318e=""
|
||||||
|
>
|
||||||
|
1 item
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="schema-item draggable"
|
||||||
|
data-test-id="run-data-schema-item"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
data-v-d00cba9a=""
|
||||||
|
type="item"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="toggle-container"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="pill"
|
||||||
|
data-depth="2"
|
||||||
|
data-name="name"
|
||||||
|
data-nest-level="1"
|
||||||
|
data-node-name="Set2"
|
||||||
|
data-node-type="n8n-nodes-base.set"
|
||||||
|
data-path=".name"
|
||||||
|
data-target="mappable"
|
||||||
|
data-test-id="run-data-schema-node-name"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
data-value="{{ $('Set2').item.json.name }}"
|
||||||
|
>
|
||||||
|
<font-awesome-icon-stub
|
||||||
|
beat="false"
|
||||||
|
beatfade="false"
|
||||||
|
border="false"
|
||||||
|
bounce="false"
|
||||||
|
class="type-icon"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
fade="false"
|
||||||
|
fixedwidth="false"
|
||||||
|
flash="false"
|
||||||
|
flip="false"
|
||||||
|
icon="font"
|
||||||
|
inverse="false"
|
||||||
|
listitem="false"
|
||||||
|
pulse="false"
|
||||||
|
shake="false"
|
||||||
|
size="sm"
|
||||||
|
spin="false"
|
||||||
|
spinpulse="false"
|
||||||
|
spinreverse="false"
|
||||||
|
swapopacity="false"
|
||||||
|
symbol="false"
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
class="title"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<span>
|
||||||
|
name
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
<span
|
||||||
|
class="text"
|
||||||
|
data-test-id="run-data-schema-item-value"
|
||||||
|
data-v-0f5e7239=""
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
|
<mark>
|
||||||
|
John
|
||||||
|
</mark>
|
||||||
|
|
||||||
|
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--teleport start-->
|
<!--teleport start-->
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
|
exports[`useFlattenSchema > flattenMultipleSchemas > should flatten node schemas 1`] = `
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"id": "Test Node",
|
||||||
|
"info": undefined,
|
||||||
|
"itemCount": [MockFunction spy],
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"preview": false,
|
||||||
|
"title": "Test Node",
|
||||||
|
"type": "header",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json }}",
|
||||||
|
"icon": "cube",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json }}",
|
||||||
|
"level": 0,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": "",
|
||||||
|
"preview": false,
|
||||||
|
"title": [MockFunction spy],
|
||||||
|
"type": "item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json.obj }}",
|
||||||
|
"icon": "cube",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json.obj }}",
|
||||||
|
"level": 1,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": ".obj",
|
||||||
|
"preview": false,
|
||||||
|
"title": "obj",
|
||||||
|
"type": "item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json.obj.foo }}",
|
||||||
|
"icon": "cube",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json.obj.foo }}",
|
||||||
|
"level": 2,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": ".obj.foo",
|
||||||
|
"preview": false,
|
||||||
|
"title": "foo",
|
||||||
|
"type": "item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": false,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json.obj.foo.nested }}",
|
||||||
|
"icon": "font",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json.obj.foo.nested }}",
|
||||||
|
"level": 3,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": ".obj.foo.nested",
|
||||||
|
"preview": false,
|
||||||
|
"title": "nested",
|
||||||
|
"type": "item",
|
||||||
|
"value": "bar",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"id": "Test Node",
|
||||||
|
"info": undefined,
|
||||||
|
"itemCount": [MockFunction spy],
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"preview": false,
|
||||||
|
"title": "Test Node",
|
||||||
|
"type": "header",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json }}",
|
||||||
|
"icon": "cube",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json }}",
|
||||||
|
"level": 0,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": "",
|
||||||
|
"preview": false,
|
||||||
|
"title": [MockFunction spy],
|
||||||
|
"type": "item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json.obj }}",
|
||||||
|
"icon": "cube",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json.obj }}",
|
||||||
|
"level": 1,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": ".obj",
|
||||||
|
"preview": false,
|
||||||
|
"title": "obj",
|
||||||
|
"type": "item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": true,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json.obj.foo }}",
|
||||||
|
"icon": "cube",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json.obj.foo }}",
|
||||||
|
"level": 2,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": ".obj.foo",
|
||||||
|
"preview": false,
|
||||||
|
"title": "foo",
|
||||||
|
"type": "item",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsable": false,
|
||||||
|
"depth": [MockFunction spy],
|
||||||
|
"expression": "{{ $('Test Node').item.json.obj.foo.nested }}",
|
||||||
|
"icon": "font",
|
||||||
|
"id": "Test Node-{{ $('Test Node').item.json.obj.foo.nested }}",
|
||||||
|
"level": 3,
|
||||||
|
"nodeName": "Test Node",
|
||||||
|
"nodeType": [MockFunction spy],
|
||||||
|
"path": ".obj.foo.nested",
|
||||||
|
"preview": false,
|
||||||
|
"title": "nested",
|
||||||
|
"type": "item",
|
||||||
|
"value": "bar",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
`;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import jp from 'jsonpath';
|
import jp from 'jsonpath';
|
||||||
import { useDataSchema, useFlattenSchema } from '@/composables/useDataSchema';
|
import { useDataSchema, useFlattenSchema, type SchemaNode } from '@/composables/useDataSchema';
|
||||||
import type { IExecutionResponse, INodeUi, Schema } from '@/Interface';
|
import type { IExecutionResponse, INodeUi, Schema } from '@/Interface';
|
||||||
import { setActivePinia } from 'pinia';
|
import { setActivePinia } from 'pinia';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import type { JSONSchema7 } from 'json-schema';
|
import type { JSONSchema7 } from 'json-schema';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
|
||||||
vi.mock('@/stores/workflows.store');
|
vi.mock('@/stores/workflows.store');
|
||||||
|
|
||||||
@@ -785,57 +786,197 @@ describe('useDataSchema', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('useFlattenSchema', () => {
|
describe('useFlattenSchema', () => {
|
||||||
it('flattens a schema', () => {
|
describe('flattenSchema', () => {
|
||||||
const schema: Schema = {
|
it('flattens a schema', () => {
|
||||||
path: '',
|
const schema: Schema = {
|
||||||
type: 'object',
|
path: '',
|
||||||
value: [
|
type: 'object',
|
||||||
{
|
value: [
|
||||||
key: 'obj',
|
{
|
||||||
path: '.obj',
|
key: 'obj',
|
||||||
type: 'object',
|
path: '.obj',
|
||||||
value: [
|
type: 'object',
|
||||||
{
|
value: [
|
||||||
key: 'foo',
|
{
|
||||||
path: '.obj.foo',
|
key: 'foo',
|
||||||
type: 'object',
|
path: '.obj.foo',
|
||||||
value: [
|
type: 'object',
|
||||||
{
|
value: [
|
||||||
key: 'nested',
|
{
|
||||||
path: '.obj.foo.nested',
|
key: 'nested',
|
||||||
type: 'string',
|
path: '.obj.foo.nested',
|
||||||
value: 'bar',
|
type: 'string',
|
||||||
},
|
value: 'bar',
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
},
|
],
|
||||||
],
|
},
|
||||||
};
|
],
|
||||||
expect(
|
};
|
||||||
useFlattenSchema().flattenSchema({
|
expect(
|
||||||
schema,
|
useFlattenSchema().flattenSchema({
|
||||||
}).length,
|
schema,
|
||||||
).toBe(3);
|
}).length,
|
||||||
|
).toBe(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('items ids should be unique', () => {
|
||||||
|
const { flattenSchema } = useFlattenSchema();
|
||||||
|
const schema: Schema = {
|
||||||
|
path: '',
|
||||||
|
type: 'object',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: 'index',
|
||||||
|
type: 'number',
|
||||||
|
value: '0',
|
||||||
|
path: '.index',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const node1Schema = flattenSchema({ schema, expressionPrefix: '$("First Node")', depth: 1 });
|
||||||
|
const node2Schema = flattenSchema({ schema, expressionPrefix: '$("Second Node")', depth: 1 });
|
||||||
|
|
||||||
|
expect(node1Schema[0].id).not.toBe(node2Schema[0].id);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('items ids should be unique', () => {
|
describe('flattenMultipleSchemas', () => {
|
||||||
const { flattenSchema } = useFlattenSchema();
|
it('should handle empty data', () => {
|
||||||
const schema: Schema = {
|
const { flattenMultipleSchemas } = useFlattenSchema();
|
||||||
path: '',
|
|
||||||
type: 'object',
|
|
||||||
value: [
|
|
||||||
{
|
|
||||||
key: 'index',
|
|
||||||
type: 'number',
|
|
||||||
value: '0',
|
|
||||||
path: '.index',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const node1Schema = flattenSchema({ schema, expressionPrefix: '$("First Node")', depth: 1 });
|
|
||||||
const node2Schema = flattenSchema({ schema, expressionPrefix: '$("Second Node")', depth: 1 });
|
|
||||||
|
|
||||||
expect(node1Schema[0].id).not.toBe(node2Schema[0].id);
|
const result = flattenMultipleSchemas(
|
||||||
|
[
|
||||||
|
mock<SchemaNode>({
|
||||||
|
node: { name: 'Test Node' },
|
||||||
|
isDataEmpty: true,
|
||||||
|
schema: { type: 'object', value: [] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
|
||||||
|
expect(result[1]).toEqual(
|
||||||
|
expect.objectContaining({ type: 'empty', key: 'emptyData', level: 1 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unexecuted nodes', () => {
|
||||||
|
const { flattenMultipleSchemas } = useFlattenSchema();
|
||||||
|
|
||||||
|
const result = flattenMultipleSchemas(
|
||||||
|
[
|
||||||
|
mock<SchemaNode>({
|
||||||
|
node: { name: 'Test Node' },
|
||||||
|
isNodeExecuted: false,
|
||||||
|
schema: { type: 'object', value: [] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
|
||||||
|
expect(result[1]).toEqual(
|
||||||
|
expect.objectContaining({ type: 'empty', key: 'executeSchema', level: 1 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty schema', () => {
|
||||||
|
const { flattenMultipleSchemas } = useFlattenSchema();
|
||||||
|
|
||||||
|
const result = flattenMultipleSchemas(
|
||||||
|
[
|
||||||
|
mock<SchemaNode>({
|
||||||
|
node: { name: 'Test Node' },
|
||||||
|
isDataEmpty: false,
|
||||||
|
hasBinary: false,
|
||||||
|
schema: { type: 'object', value: [] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
|
||||||
|
expect(result[1]).toEqual(
|
||||||
|
expect.objectContaining({ type: 'empty', key: 'emptySchema', level: 1 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty schema with binary', () => {
|
||||||
|
const { flattenMultipleSchemas } = useFlattenSchema();
|
||||||
|
|
||||||
|
const result = flattenMultipleSchemas(
|
||||||
|
[
|
||||||
|
mock<SchemaNode>({
|
||||||
|
node: { name: 'Test Node' },
|
||||||
|
isDataEmpty: false,
|
||||||
|
hasBinary: true,
|
||||||
|
schema: { type: 'object', value: [] },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result[0]).toEqual(expect.objectContaining({ type: 'header', title: 'Test Node' }));
|
||||||
|
expect(result[1]).toEqual(
|
||||||
|
expect.objectContaining({ type: 'empty', key: 'emptySchemaWithBinary', level: 1 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flatten node schemas', () => {
|
||||||
|
const { flattenMultipleSchemas } = useFlattenSchema();
|
||||||
|
const schema: Schema = {
|
||||||
|
path: '',
|
||||||
|
type: 'object',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: 'obj',
|
||||||
|
path: '.obj',
|
||||||
|
type: 'object',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: 'foo',
|
||||||
|
path: '.obj.foo',
|
||||||
|
type: 'object',
|
||||||
|
value: [
|
||||||
|
{
|
||||||
|
key: 'nested',
|
||||||
|
path: '.obj.foo.nested',
|
||||||
|
type: 'string',
|
||||||
|
value: 'bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = flattenMultipleSchemas(
|
||||||
|
[
|
||||||
|
mock<SchemaNode>({
|
||||||
|
node: { name: 'Test Node' },
|
||||||
|
isDataEmpty: false,
|
||||||
|
hasBinary: false,
|
||||||
|
preview: false,
|
||||||
|
schema,
|
||||||
|
}),
|
||||||
|
mock<SchemaNode>({
|
||||||
|
node: { name: 'Test Node' },
|
||||||
|
isDataEmpty: false,
|
||||||
|
hasBinary: false,
|
||||||
|
preview: false,
|
||||||
|
schema,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
vi.fn(),
|
||||||
|
);
|
||||||
|
expect(result).toHaveLength(10);
|
||||||
|
expect(result.filter((item) => item.type === 'header')).toHaveLength(2);
|
||||||
|
expect(result.filter((item) => item.type === 'item')).toHaveLength(8);
|
||||||
|
expect(result).toMatchSnapshot();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -235,6 +235,8 @@ export type SchemaNode = {
|
|||||||
preview: boolean;
|
preview: boolean;
|
||||||
isNodeExecuted: boolean;
|
isNodeExecuted: boolean;
|
||||||
hasBinary: boolean;
|
hasBinary: boolean;
|
||||||
|
runIndex: number;
|
||||||
|
isDataEmpty: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RenderItem = {
|
export type RenderItem = {
|
||||||
@@ -285,7 +287,7 @@ export type RenderEmpty = {
|
|||||||
type: 'empty';
|
type: 'empty';
|
||||||
level: number;
|
level: number;
|
||||||
nodeName: string;
|
nodeName: string;
|
||||||
key: 'emptyData' | 'emptyDataWithBinary' | 'executeSchema';
|
key: 'emptyData' | 'emptySchema' | 'emptySchemaWithBinary' | 'executeSchema';
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Renders = RenderHeader | RenderItem | RenderIcon | RenderNotice | RenderEmpty;
|
export type Renders = RenderHeader | RenderItem | RenderIcon | RenderNotice | RenderEmpty;
|
||||||
@@ -323,7 +325,7 @@ const moreFieldsItem = (): RenderIcon => ({
|
|||||||
tooltip: useI18n().baseText('dataMapping.schemaView.previewExtraFields'),
|
tooltip: useI18n().baseText('dataMapping.schemaView.previewExtraFields'),
|
||||||
});
|
});
|
||||||
|
|
||||||
const isDataEmpty = (schema: Schema) => {
|
const isEmptySchema = (schema: Schema) => {
|
||||||
// Utilize the generated schema instead of looping over the entire data again
|
// Utilize the generated schema instead of looping over the entire data again
|
||||||
// The schema for empty data is { type: 'object', value: [] }
|
// The schema for empty data is { type: 'object', value: [] }
|
||||||
const isObjectOrArray = schema.type === 'object';
|
const isObjectOrArray = schema.type === 'object';
|
||||||
@@ -336,18 +338,8 @@ const prefixTitle = (title: string, prefix?: string) => (prefix ? `${prefix}[${t
|
|||||||
|
|
||||||
export const useFlattenSchema = () => {
|
export const useFlattenSchema = () => {
|
||||||
const closedNodes = ref<Set<string>>(new Set());
|
const closedNodes = ref<Set<string>>(new Set());
|
||||||
const headerIds = ref<Set<string>>(new Set());
|
|
||||||
const toggleLeaf = (id: string) => {
|
|
||||||
if (closedNodes.value.has(id)) {
|
|
||||||
closedNodes.value.delete(id);
|
|
||||||
} else {
|
|
||||||
closedNodes.value.add(id);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleNode = (id: string) => {
|
const toggleNode = (id: string) => {
|
||||||
if (closedNodes.value.has(id)) {
|
if (closedNodes.value.has(id)) {
|
||||||
closedNodes.value = new Set(headerIds.value);
|
|
||||||
closedNodes.value.delete(id);
|
closedNodes.value.delete(id);
|
||||||
} else {
|
} else {
|
||||||
closedNodes.value.add(id);
|
closedNodes.value.add(id);
|
||||||
@@ -374,7 +366,7 @@ export const useFlattenSchema = () => {
|
|||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
}): Renders[] => {
|
}): Renders[] => {
|
||||||
// only show empty item for the first level
|
// only show empty item for the first level
|
||||||
if (isDataEmpty(schema) && level < 0) {
|
if (isEmptySchema(schema) && level < 0) {
|
||||||
return [emptyItem('emptyData')];
|
return [emptyItem('emptyData')];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -450,8 +442,6 @@ export const useFlattenSchema = () => {
|
|||||||
nodes: SchemaNode[],
|
nodes: SchemaNode[],
|
||||||
additionalInfo: (node: INodeUi) => string,
|
additionalInfo: (node: INodeUi) => string,
|
||||||
) => {
|
) => {
|
||||||
headerIds.value.clear();
|
|
||||||
|
|
||||||
return nodes.reduce<Renders[]>((acc, item) => {
|
return nodes.reduce<Renders[]>((acc, item) => {
|
||||||
acc.push({
|
acc.push({
|
||||||
title: item.node.name,
|
title: item.node.name,
|
||||||
@@ -464,20 +454,21 @@ export const useFlattenSchema = () => {
|
|||||||
preview: item.preview,
|
preview: item.preview,
|
||||||
});
|
});
|
||||||
|
|
||||||
headerIds.value.add(item.node.name);
|
|
||||||
|
|
||||||
if (closedNodes.value.has(item.node.name)) {
|
if (closedNodes.value.has(item.node.name)) {
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isDataEmpty(item.schema) && !item.isNodeExecuted && !item.hasBinary) {
|
if (isEmptySchema(item.schema)) {
|
||||||
acc.push(emptyItem('executeSchema', { nodeName: item.node.name, level: 1 }));
|
if (!item.isNodeExecuted) {
|
||||||
return acc;
|
acc.push(emptyItem('executeSchema', { level: 1 }));
|
||||||
}
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
if (isDataEmpty(item.schema)) {
|
if (item.isDataEmpty) {
|
||||||
const key = item.hasBinary ? 'emptyDataWithBinary' : 'emptyData';
|
acc.push(emptyItem('emptyData', { level: 1 }));
|
||||||
acc.push(emptyItem(key, { level: 1 }));
|
return acc;
|
||||||
|
}
|
||||||
|
acc.push(emptyItem(item.hasBinary ? 'emptySchemaWithBinary' : 'emptySchema', { level: 1 }));
|
||||||
return acc;
|
return acc;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +496,6 @@ export const useFlattenSchema = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
closedNodes,
|
closedNodes,
|
||||||
toggleLeaf,
|
|
||||||
toggleNode,
|
toggleNode,
|
||||||
flattenSchema,
|
flattenSchema,
|
||||||
flattenMultipleSchemas,
|
flattenMultipleSchemas,
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
|||||||
import { createTestNode } from '@/__tests__/mocks';
|
import { createTestNode } from '@/__tests__/mocks';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
import { CUSTOM_API_CALL_KEY } from '@/constants';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
vi.mock('@/stores/workflows.store', () => ({
|
import { mock } from 'vitest-mock-extended';
|
||||||
useWorkflowsStore: vi.fn(),
|
import type { ExecutionStatus, IRunData } from 'n8n-workflow';
|
||||||
}));
|
|
||||||
|
|
||||||
describe('useNodeHelpers()', () => {
|
describe('useNodeHelpers()', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -55,42 +54,8 @@ describe('useNodeHelpers()', () => {
|
|||||||
expect(result).toEqual([]);
|
expect(result).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return an empty array when workflowsStore.getWorkflowExecution() is null', () => {
|
it('should return an empty array when runData is not available', () => {
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
mockedStore(useWorkflowsStore).getWorkflowRunData = null;
|
||||||
getWorkflowExecution: null,
|
|
||||||
} as ReturnType<typeof useWorkflowsStore>);
|
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
|
||||||
const node = createTestNode({
|
|
||||||
name: 'test',
|
|
||||||
type: 'test',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = getNodeInputData(node);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an empty array when workflowsStore.getWorkflowExecution() is null', () => {
|
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
|
||||||
getWorkflowExecution: null,
|
|
||||||
} as ReturnType<typeof useWorkflowsStore>);
|
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
|
||||||
const node = createTestNode({
|
|
||||||
name: 'test',
|
|
||||||
type: 'test',
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = getNodeInputData(node);
|
|
||||||
expect(result).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return an empty array when resultData is not available', () => {
|
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
|
||||||
getWorkflowExecution: {
|
|
||||||
data: {
|
|
||||||
resultData: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as ReturnType<typeof useWorkflowsStore>);
|
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
const { getNodeInputData } = useNodeHelpers();
|
||||||
const node = createTestNode({
|
const node = createTestNode({
|
||||||
name: 'test',
|
name: 'test',
|
||||||
@@ -103,17 +68,9 @@ describe('useNodeHelpers()', () => {
|
|||||||
|
|
||||||
it('should return an empty array when taskData is unavailable', () => {
|
it('should return an empty array when taskData is unavailable', () => {
|
||||||
const nodeName = 'Code';
|
const nodeName = 'Code';
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
getWorkflowExecution: {
|
[nodeName]: [],
|
||||||
data: {
|
});
|
||||||
resultData: {
|
|
||||||
runData: {
|
|
||||||
[nodeName]: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as ReturnType<typeof useWorkflowsStore>);
|
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
const { getNodeInputData } = useNodeHelpers();
|
||||||
const node = createTestNode({
|
const node = createTestNode({
|
||||||
name: nodeName,
|
name: nodeName,
|
||||||
@@ -126,17 +83,9 @@ describe('useNodeHelpers()', () => {
|
|||||||
|
|
||||||
it('should return an empty array when taskData.data is unavailable', () => {
|
it('should return an empty array when taskData.data is unavailable', () => {
|
||||||
const nodeName = 'Code';
|
const nodeName = 'Code';
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
getWorkflowExecution: {
|
[nodeName]: [{ data: undefined }],
|
||||||
data: {
|
});
|
||||||
resultData: {
|
|
||||||
runData: {
|
|
||||||
[nodeName]: [{ data: undefined }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as ReturnType<typeof useWorkflowsStore>);
|
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
const { getNodeInputData } = useNodeHelpers();
|
||||||
const node = createTestNode({
|
const node = createTestNode({
|
||||||
name: nodeName,
|
name: nodeName,
|
||||||
@@ -149,24 +98,16 @@ describe('useNodeHelpers()', () => {
|
|||||||
|
|
||||||
it('should return input data from inputOverride', () => {
|
it('should return input data from inputOverride', () => {
|
||||||
const nodeName = 'Code';
|
const nodeName = 'Code';
|
||||||
const data = { hello: 'world' };
|
const data = [{ json: { hello: 'world' } }];
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
getWorkflowExecution: {
|
[nodeName]: [
|
||||||
data: {
|
{
|
||||||
resultData: {
|
inputOverride: {
|
||||||
runData: {
|
main: [data],
|
||||||
[nodeName]: [
|
|
||||||
{
|
|
||||||
inputOverride: {
|
|
||||||
main: [data],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
} as unknown as ReturnType<typeof useWorkflowsStore>);
|
});
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
const { getNodeInputData } = useNodeHelpers();
|
||||||
const node = createTestNode({
|
const node = createTestNode({
|
||||||
name: nodeName,
|
name: nodeName,
|
||||||
@@ -180,24 +121,10 @@ describe('useNodeHelpers()', () => {
|
|||||||
it.each(['example', 'example.withdot', 'example.with.dots', 'example.with.dots and spaces'])(
|
it.each(['example', 'example.withdot', 'example.with.dots', 'example.with.dots and spaces'])(
|
||||||
'should return input data for "%s" node name, with given connection type and output index',
|
'should return input data for "%s" node name, with given connection type and output index',
|
||||||
(nodeName) => {
|
(nodeName) => {
|
||||||
const data = { hello: 'world' };
|
const data = [{ json: { hello: 'world' } }];
|
||||||
vi.mocked(useWorkflowsStore).mockReturnValue({
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
getWorkflowExecution: {
|
[nodeName]: [{ data: { main: [data] } }],
|
||||||
data: {
|
});
|
||||||
resultData: {
|
|
||||||
runData: {
|
|
||||||
[nodeName]: [
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
main: [data],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} as unknown as ReturnType<typeof useWorkflowsStore>);
|
|
||||||
const { getNodeInputData } = useNodeHelpers();
|
const { getNodeInputData } = useNodeHelpers();
|
||||||
const node = createTestNode({
|
const node = createTestNode({
|
||||||
name: nodeName,
|
name: nodeName,
|
||||||
@@ -210,6 +137,92 @@ describe('useNodeHelpers()', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getLastRunIndexWithData()', () => {
|
||||||
|
const mockData = [{ json: { hello: 'world' } }];
|
||||||
|
it('should return the last runIndex with data', () => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||||
|
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
|
[nodeName]: [{ data: { main: [mockData] } }, { data: { main: [mockData] } }],
|
||||||
|
});
|
||||||
|
expect(getLastRunIndexWithData(nodeName)).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return -1 when there are no runs', () => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||||
|
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
|
[nodeName]: [],
|
||||||
|
});
|
||||||
|
expect(getLastRunIndexWithData(nodeName)).toEqual(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return -1 when there is no runData', () => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||||
|
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = null;
|
||||||
|
expect(getLastRunIndexWithData(nodeName)).toEqual(-1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with custom outputIndex', () => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||||
|
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
|
[nodeName]: [
|
||||||
|
{ data: { main: [mockData, []] } },
|
||||||
|
{ data: { main: [mockData, []] } },
|
||||||
|
{ data: { main: [mockData, mockData] } },
|
||||||
|
{ data: { main: [[], mockData] } },
|
||||||
|
{ data: { main: [[], []] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(getLastRunIndexWithData(nodeName, 1)).toEqual(3);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with custom connectionType', () => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
const { getLastRunIndexWithData } = useNodeHelpers();
|
||||||
|
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
|
[nodeName]: [
|
||||||
|
{ data: { main: [mockData], ai_tool: [mockData] } },
|
||||||
|
{ data: { ai_tool: [mockData] } },
|
||||||
|
{ data: { main: [mockData] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
expect(getLastRunIndexWithData(nodeName, 0, 'ai_tool')).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasNodeExecuted()', () => {
|
||||||
|
it('should return false when runData is not available', () => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = null;
|
||||||
|
const { hasNodeExecuted } = useNodeHelpers();
|
||||||
|
expect(hasNodeExecuted(nodeName)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<{ status?: ExecutionStatus; expected: boolean }>([
|
||||||
|
{ status: undefined, expected: false },
|
||||||
|
{ status: 'waiting', expected: false },
|
||||||
|
{ status: 'running', expected: false },
|
||||||
|
{ status: 'error', expected: true },
|
||||||
|
{ status: 'success', expected: true },
|
||||||
|
])('should return $expected when execution status is $status', ({ status, expected }) => {
|
||||||
|
const nodeName = 'Test Node';
|
||||||
|
|
||||||
|
mockedStore(useWorkflowsStore).getWorkflowRunData = mock<IRunData>({
|
||||||
|
[nodeName]: [{ executionStatus: status }],
|
||||||
|
});
|
||||||
|
const { hasNodeExecuted } = useNodeHelpers();
|
||||||
|
expect(hasNodeExecuted(nodeName)).toBe(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('assignNodeId()', () => {
|
describe('assignNodeId()', () => {
|
||||||
it('should assign a unique id to the node', () => {
|
it('should assign a unique id to the node', () => {
|
||||||
const { assignNodeId } = useNodeHelpers();
|
const { assignNodeId } = useNodeHelpers();
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from 'vue';
|
||||||
import { useHistoryStore } from '@/stores/history.store';
|
import { useHistoryStore } from '@/stores/history.store';
|
||||||
import {
|
import { CUSTOM_API_CALL_KEY, PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
|
||||||
CUSTOM_API_CALL_KEY,
|
|
||||||
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
|
||||||
SPLIT_IN_BATCHES_NODE_TYPE,
|
|
||||||
} from '@/constants';
|
|
||||||
|
|
||||||
import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionTypes } from 'n8n-workflow';
|
import { NodeHelpers, ExpressionEvaluatorProxy, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
import type {
|
import type {
|
||||||
@@ -546,27 +542,33 @@ export function useNodeHelpers() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeTaskData(node: INodeUi | null, runIndex = 0) {
|
function getNodeTaskData(nodeName: string, runIndex = 0) {
|
||||||
if (node === null) {
|
return getAllNodeTaskData(nodeName)?.[runIndex] ?? null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
if (workflowsStore.getWorkflowExecution === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const executionData = workflowsStore.getWorkflowExecution.data;
|
function getAllNodeTaskData(nodeName: string) {
|
||||||
if (!executionData?.resultData) {
|
return workflowsStore.getWorkflowRunData?.[nodeName] ?? null;
|
||||||
// unknown status
|
}
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const runData = executionData.resultData.runData;
|
|
||||||
|
|
||||||
const taskData = get(runData, [node.name, runIndex]);
|
function hasNodeExecuted(nodeName: string) {
|
||||||
if (!taskData) {
|
return (
|
||||||
return null;
|
getAllNodeTaskData(nodeName)?.some(
|
||||||
}
|
({ executionStatus }) => executionStatus && ['success', 'error'].includes(executionStatus),
|
||||||
|
) ?? false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return taskData;
|
function getLastRunIndexWithData(
|
||||||
|
nodeName: string,
|
||||||
|
outputIndex = 0,
|
||||||
|
connectionType: NodeConnectionType = NodeConnectionTypes.Main,
|
||||||
|
) {
|
||||||
|
const allTaskData = getAllNodeTaskData(nodeName) ?? [];
|
||||||
|
|
||||||
|
return allTaskData.findLastIndex(
|
||||||
|
(taskData) =>
|
||||||
|
taskData.data && getInputData(taskData.data, outputIndex, connectionType).length > 0,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getNodeInputData(
|
function getNodeInputData(
|
||||||
@@ -576,17 +578,8 @@ export function useNodeHelpers() {
|
|||||||
paneType: NodePanelType = 'output',
|
paneType: NodePanelType = 'output',
|
||||||
connectionType: NodeConnectionType = NodeConnectionTypes.Main,
|
connectionType: NodeConnectionType = NodeConnectionTypes.Main,
|
||||||
): INodeExecutionData[] {
|
): INodeExecutionData[] {
|
||||||
//TODO: check if this needs to be fixed in different place
|
if (!node) return [];
|
||||||
if (
|
const taskData = getNodeTaskData(node.name, runIndex);
|
||||||
node?.type === SPLIT_IN_BATCHES_NODE_TYPE &&
|
|
||||||
paneType === 'input' &&
|
|
||||||
runIndex !== 0 &&
|
|
||||||
outputIndex !== 0
|
|
||||||
) {
|
|
||||||
runIndex = runIndex - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
const taskData = getNodeTaskData(node, runIndex);
|
|
||||||
if (taskData === null) {
|
if (taskData === null) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -1012,6 +1005,8 @@ export function useNodeHelpers() {
|
|||||||
disableNodes,
|
disableNodes,
|
||||||
getNodeSubtitle,
|
getNodeSubtitle,
|
||||||
updateNodesCredentialsIssues,
|
updateNodesCredentialsIssues,
|
||||||
|
getLastRunIndexWithData,
|
||||||
|
hasNodeExecuted,
|
||||||
getNodeInputData,
|
getNodeInputData,
|
||||||
matchCredentials,
|
matchCredentials,
|
||||||
isInsertingNodes,
|
isInsertingNodes,
|
||||||
|
|||||||
@@ -667,8 +667,9 @@
|
|||||||
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
"dataMapping.tableView.tableColumnsExceeded": "Some columns are hidden",
|
||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip": "Your data has more than {columnLimit} columns so some are hidden. Switch to {link} to see all data.",
|
||||||
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
|
"dataMapping.tableView.tableColumnsExceeded.tooltip.link": "JSON view",
|
||||||
"dataMapping.schemaView.emptyData": "No fields - item(s) exist, but they're empty",
|
"dataMapping.schemaView.emptyData": "No fields - node executed, but no items were sent on this branch",
|
||||||
"dataMapping.schemaView.emptyDataWithBinary": "Only binary data exists. View it using the 'Binary' tab",
|
"dataMapping.schemaView.emptySchema": "No fields - item(s) exist, but they're empty",
|
||||||
|
"dataMapping.schemaView.emptySchemaWithBinary": "Only binary data exists. View it using the 'Binary' tab",
|
||||||
"dataMapping.schemaView.executeSchema": "{link} to see schema",
|
"dataMapping.schemaView.executeSchema": "{link} to see schema",
|
||||||
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
|
"dataMapping.schemaView.disabled": "This node is disabled and will just pass data through",
|
||||||
"dataMapping.schemaView.noMatches": "No results for '{search}'",
|
"dataMapping.schemaView.noMatches": "No results for '{search}'",
|
||||||
|
|||||||
Reference in New Issue
Block a user