mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
perf(editor): Virtualize SchemaView (#11694)
This commit is contained in:
parent
3a5bd12945
commit
9c6def9197
|
@ -172,4 +172,5 @@ export interface FrontendSettings {
|
||||||
blockFileAccessToN8nFiles: boolean;
|
blockFileAccessToN8nFiles: boolean;
|
||||||
};
|
};
|
||||||
betaFeatures: FrontendBetaFeatures[];
|
betaFeatures: FrontendBetaFeatures[];
|
||||||
|
virtualSchemaView: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
@ -405,4 +405,11 @@ export const schema = {
|
||||||
doc: 'Set this to 1 to enable the new partial execution logic by default.',
|
doc: 'Set this to 1 to enable the new partial execution logic by default.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
virtualSchemaView: {
|
||||||
|
doc: 'Whether to display the virtualized schema view',
|
||||||
|
format: Boolean,
|
||||||
|
default: false,
|
||||||
|
env: 'N8N_VIRTUAL_SCHEMA_VIEW',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -231,6 +231,7 @@ export class FrontendService {
|
||||||
blockFileAccessToN8nFiles: this.securityConfig.blockFileAccessToN8nFiles,
|
blockFileAccessToN8nFiles: this.securityConfig.blockFileAccessToN8nFiles,
|
||||||
},
|
},
|
||||||
betaFeatures: this.frontendConfig.betaFeatures,
|
betaFeatures: this.frontendConfig.betaFeatures,
|
||||||
|
virtualSchemaView: config.getEnv('virtualSchemaView'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
"vue-json-pretty": "2.2.4",
|
"vue-json-pretty": "2.2.4",
|
||||||
"vue-markdown-render": "catalog:frontend",
|
"vue-markdown-render": "catalog:frontend",
|
||||||
"vue-router": "catalog:frontend",
|
"vue-router": "catalog:frontend",
|
||||||
|
"vue-virtual-scroller": "2.0.0-beta.8",
|
||||||
"vue3-touch-events": "^4.1.3",
|
"vue3-touch-events": "^4.1.3",
|
||||||
"xss": "catalog:"
|
"xss": "catalog:"
|
||||||
},
|
},
|
||||||
|
|
|
@ -126,4 +126,5 @@ export const defaultSettings: FrontendSettings = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
betaFeatures: [],
|
betaFeatures: [],
|
||||||
|
virtualSchemaView: false,
|
||||||
};
|
};
|
||||||
|
|
|
@ -46,6 +46,7 @@ import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useNodeType } from '@/composables/useNodeType';
|
import { useNodeType } from '@/composables/useNodeType';
|
||||||
|
import { useSettingsStore } from '@/stores/settings.store';
|
||||||
import type { PinDataSource, UnpinDataSource } from '@/composables/usePinnedData';
|
import type { PinDataSource, UnpinDataSource } from '@/composables/usePinnedData';
|
||||||
import { usePinnedData } from '@/composables/usePinnedData';
|
import { usePinnedData } from '@/composables/usePinnedData';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
@ -86,8 +87,11 @@ const LazyRunDataTable = defineAsyncComponent(
|
||||||
const LazyRunDataJson = defineAsyncComponent(
|
const LazyRunDataJson = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataJson.vue'),
|
async () => await import('@/components/RunDataJson.vue'),
|
||||||
);
|
);
|
||||||
const LazyRunDataSchema = defineAsyncComponent(
|
|
||||||
async () => await import('@/components/RunDataSchema.vue'),
|
const LazyRunDataSchema = defineAsyncComponent(async () =>
|
||||||
|
useSettingsStore().settings.virtualSchemaView
|
||||||
|
? await import('@/components/VirtualSchema.vue')
|
||||||
|
: await import('@/components/RunDataSchema.vue'),
|
||||||
);
|
);
|
||||||
const LazyRunDataHtml = defineAsyncComponent(
|
const LazyRunDataHtml = defineAsyncComponent(
|
||||||
async () => await import('@/components/RunDataHtml.vue'),
|
async () => await import('@/components/RunDataHtml.vue'),
|
||||||
|
|
439
packages/editor-ui/src/components/VirtualSchema.test.ts
Normal file
439
packages/editor-ui/src/components/VirtualSchema.test.ts
Normal file
|
@ -0,0 +1,439 @@
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import VirtualSchema from '@/components/VirtualSchema.vue';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { userEvent } from '@testing-library/user-event';
|
||||||
|
import { cleanup, waitFor } from '@testing-library/vue';
|
||||||
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
|
import {
|
||||||
|
createTestNode,
|
||||||
|
defaultNodeDescriptions,
|
||||||
|
mockNodeTypeDescription,
|
||||||
|
} from '@/__tests__/mocks';
|
||||||
|
import { IF_NODE_TYPE, SET_NODE_TYPE, MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import type { IWorkflowDb } from '@/Interface';
|
||||||
|
import { NodeConnectionType, type IDataObject, type INodeExecutionData } from 'n8n-workflow';
|
||||||
|
import * as nodeHelpers from '@/composables/useNodeHelpers';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { fireEvent } from '@testing-library/dom';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
|
||||||
|
const mockNode1 = createTestNode({
|
||||||
|
name: 'Manual Trigger',
|
||||||
|
type: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockNode2 = createTestNode({
|
||||||
|
name: 'Set2',
|
||||||
|
type: SET_NODE_TYPE,
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const disabledNode = createTestNode({
|
||||||
|
name: 'Disabled Node',
|
||||||
|
type: SET_NODE_TYPE,
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ifNode = createTestNode({
|
||||||
|
name: 'If',
|
||||||
|
type: IF_NODE_TYPE,
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const aiTool = createTestNode({
|
||||||
|
name: 'AI Tool',
|
||||||
|
type: '@n8n/n8n-nodes-langchain.memoryBufferWindow',
|
||||||
|
typeVersion: 1,
|
||||||
|
disabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const unknownNodeType = createTestNode({
|
||||||
|
name: 'Unknown Node Type',
|
||||||
|
type: 'unknown',
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultNodes = [
|
||||||
|
{ name: 'Manual Trigger', indicies: [], depth: 1 },
|
||||||
|
{ name: 'Set2', indicies: [], depth: 2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
async function setupStore() {
|
||||||
|
const workflow = mock<IWorkflowDb>({
|
||||||
|
id: '123',
|
||||||
|
name: 'Test Workflow',
|
||||||
|
connections: {},
|
||||||
|
active: true,
|
||||||
|
nodes: [mockNode1, mockNode2, disabledNode, ifNode, aiTool, unknownNodeType],
|
||||||
|
});
|
||||||
|
|
||||||
|
const pinia = createPinia();
|
||||||
|
setActivePinia(pinia);
|
||||||
|
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
|
||||||
|
nodeTypesStore.setNodeTypes([
|
||||||
|
...defaultNodeDescriptions,
|
||||||
|
mockNodeTypeDescription({
|
||||||
|
name: MANUAL_TRIGGER_NODE_TYPE,
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
}),
|
||||||
|
mockNodeTypeDescription({
|
||||||
|
name: IF_NODE_TYPE,
|
||||||
|
outputs: [NodeConnectionType.Main, NodeConnectionType.Main],
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
workflowsStore.workflow = workflow;
|
||||||
|
|
||||||
|
return pinia;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mockNodeOutputData(nodeName: string, data: IDataObject[], outputIndex = 0) {
|
||||||
|
const originalNodeHelpers = nodeHelpers.useNodeHelpers();
|
||||||
|
vi.spyOn(nodeHelpers, 'useNodeHelpers').mockImplementation(() => {
|
||||||
|
return {
|
||||||
|
...originalNodeHelpers,
|
||||||
|
getNodeInputData: vi.fn((node, _, output) => {
|
||||||
|
if (node.name === nodeName && output === outputIndex) {
|
||||||
|
return data.map((json) => ({ json }));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('RunDataSchema.vue', () => {
|
||||||
|
let renderComponent: ReturnType<typeof createComponentRenderer>;
|
||||||
|
|
||||||
|
const DynamicScrollerStub = {
|
||||||
|
props: {
|
||||||
|
items: Array,
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
'<div><template v-for="item in items"><slot v-bind="{ item }"></slot></template></div>',
|
||||||
|
methods: {
|
||||||
|
scrollToItem: vi.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DynamicScrollerItemStub = {
|
||||||
|
template: '<slot></slot>',
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
cleanup();
|
||||||
|
renderComponent = createComponentRenderer(VirtualSchema, {
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
DynamicScroller: DynamicScrollerStub,
|
||||||
|
DynamicScrollerItem: DynamicScrollerItemStub,
|
||||||
|
FontAwesomeIcon: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pinia: await setupStore(),
|
||||||
|
props: {
|
||||||
|
mappingEnabled: true,
|
||||||
|
runIndex: 1,
|
||||||
|
outputIndex: 0,
|
||||||
|
totalRuns: 2,
|
||||||
|
paneType: 'input',
|
||||||
|
connectionType: 'main',
|
||||||
|
search: '',
|
||||||
|
nodes: defaultNodes,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders schema for empty data', async () => {
|
||||||
|
const { getAllByText, getAllByTestId } = renderComponent();
|
||||||
|
|
||||||
|
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(2);
|
||||||
|
|
||||||
|
// Collapse second node
|
||||||
|
await userEvent.click(getAllByTestId('run-data-schema-header')[1]);
|
||||||
|
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders schema for data', async () => {
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [
|
||||||
|
{ json: { name: 'John', age: 22, hobbies: ['surfing', 'traveling'] } },
|
||||||
|
{ json: { name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode2,
|
||||||
|
data: [
|
||||||
|
{ json: { name: 'John', age: 22, hobbies: ['surfing', 'traveling'] } },
|
||||||
|
{ json: { name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent();
|
||||||
|
const headers = getAllByTestId('run-data-schema-header');
|
||||||
|
expect(headers.length).toBe(2);
|
||||||
|
expect(headers[0]).toHaveTextContent('Manual Trigger');
|
||||||
|
expect(headers[0]).toHaveTextContent('2 items');
|
||||||
|
expect(headers[1]).toHaveTextContent('Set2');
|
||||||
|
|
||||||
|
const items = getAllByTestId('run-data-schema-item');
|
||||||
|
|
||||||
|
expect(items[0]).toHaveTextContent('nameJohn');
|
||||||
|
expect(items[1]).toHaveTextContent('age22');
|
||||||
|
expect(items[2]).toHaveTextContent('hobbies');
|
||||||
|
expect(items[3]).toHaveTextContent('hobbies[0]surfing');
|
||||||
|
expect(items[4]).toHaveTextContent('hobbies[1]traveling');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders schema in output pane', async () => {
|
||||||
|
const { container } = renderComponent({
|
||||||
|
props: {
|
||||||
|
nodes: [],
|
||||||
|
paneType: 'output',
|
||||||
|
node: mockNode1,
|
||||||
|
data: [
|
||||||
|
{ name: 'John', age: 22, hobbies: ['surfing', 'traveling'] },
|
||||||
|
{ name: 'Joe', age: 33, hobbies: ['skateboarding', 'gaming'] },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders schema with spaces and dots', () => {
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [
|
||||||
|
{
|
||||||
|
json: {
|
||||||
|
'hello world': [
|
||||||
|
{
|
||||||
|
test: {
|
||||||
|
'more to think about': 1,
|
||||||
|
},
|
||||||
|
'test.how': 'ignore',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { container } = renderComponent();
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders no data to show for data empty objects', () => {
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [{ json: {} }, { json: {} }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByText } = renderComponent();
|
||||||
|
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
// this can happen when setting the output to [{}]
|
||||||
|
it('renders empty state to show for empty data', () => {
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [{} as INodeExecutionData],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByText } = renderComponent({ props: { paneType: 'output' } });
|
||||||
|
expect(getAllByText("No fields - item(s) exist, but they're empty").length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders disabled nodes correctly', () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
nodes: [{ name: disabledNode.name, indicies: [], depth: 1 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('run-data-schema-header')).toHaveTextContent(
|
||||||
|
`${disabledNode.name} (Deactivated)`,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders schema for correct output branch', async () => {
|
||||||
|
mockNodeOutputData(
|
||||||
|
'If',
|
||||||
|
[
|
||||||
|
{ id: 1, name: 'John' },
|
||||||
|
{ id: 2, name: 'Jane' },
|
||||||
|
],
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
nodes: [{ name: 'If', indicies: [1], depth: 2 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('run-data-schema-header')).toHaveTextContent('If');
|
||||||
|
expect(getByTestId('run-data-schema-header')).toHaveTextContent('2 items');
|
||||||
|
expect(getByTestId('run-data-schema-header')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders previous nodes schema for AI tools', async () => {
|
||||||
|
mockNodeOutputData(
|
||||||
|
'If',
|
||||||
|
[
|
||||||
|
{ id: 1, name: 'John' },
|
||||||
|
{ id: 2, name: 'Jane' },
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
nodes: [
|
||||||
|
{
|
||||||
|
name: 'If',
|
||||||
|
indicies: [], // indicies are not set for AI tools
|
||||||
|
depth: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
node: aiTool,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(getByTestId('run-data-schema-header')).toHaveTextContent('If');
|
||||||
|
expect(getByTestId('run-data-schema-header')).toHaveTextContent('2 items');
|
||||||
|
expect(getByTestId('run-data-schema-header')).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders its own data for AI tools in debug mode', async () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
nodes: [], // in debug mode nodes are empty
|
||||||
|
node: aiTool,
|
||||||
|
data: [{ output: 'AI tool output' }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('run-data-schema-item')).toHaveTextContent('AI tool output');
|
||||||
|
});
|
||||||
|
|
||||||
|
test.each([[[{ tx: false }, { tx: false }]], [[{ tx: '' }, { tx: '' }]], [[{ tx: [] }]]])(
|
||||||
|
'renders schema instead of showing no data for %o',
|
||||||
|
(data) => {
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: data.map((item) => ({ json: item })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent();
|
||||||
|
expect(getAllByTestId('run-data-schema-item')[0]).toHaveTextContent('tx');
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should filter invalid connections', () => {
|
||||||
|
const { pinData } = useWorkflowsStore();
|
||||||
|
pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [{ json: { tx: 1 } }],
|
||||||
|
});
|
||||||
|
pinData({
|
||||||
|
node: mockNode2,
|
||||||
|
data: [{ json: { tx: 2 } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
nodes: [
|
||||||
|
{ name: mockNode1.name, indicies: [], depth: 1 },
|
||||||
|
{ name: 'unknown', indicies: [], depth: 1 },
|
||||||
|
{ name: mockNode2.name, indicies: [], depth: 1 },
|
||||||
|
{ name: unknownNodeType.name, indicies: [], depth: 1 },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getAllByTestId('run-data-schema-item').length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show connections', () => {
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
vi.spyOn(ndvStore, 'ndvNodeInputNumber', 'get').mockReturnValue({
|
||||||
|
[defaultNodes[0].name]: [0],
|
||||||
|
[defaultNodes[1].name]: [0, 1, 2],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByTestId } = renderComponent();
|
||||||
|
const headers = getAllByTestId('run-data-schema-header');
|
||||||
|
expect(headers.length).toBe(2);
|
||||||
|
expect(headers[0]).toHaveTextContent('Input 0');
|
||||||
|
expect(headers[1]).toHaveTextContent('Inputs 0, 1, 2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle drop event', async () => {
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [{ json: { name: 'John', age: 22, hobbies: ['surfing', 'traveling'] } }],
|
||||||
|
});
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const trackSpy = vi.spyOn(telemetry, 'track');
|
||||||
|
const reset = vi.spyOn(ndvStore, 'resetMappingTelemetry');
|
||||||
|
const { getAllByTestId } = renderComponent();
|
||||||
|
|
||||||
|
const items = getAllByTestId('run-data-schema-item');
|
||||||
|
expect(items.length).toBe(6);
|
||||||
|
|
||||||
|
expect(items[0].className).toBe('schema-item draggable');
|
||||||
|
expect(items[0]).toHaveTextContent('nameJohn');
|
||||||
|
|
||||||
|
const pill = items[0].querySelector('.pill') as Element;
|
||||||
|
|
||||||
|
fireEvent(pill, new MouseEvent('mousedown', { bubbles: true }));
|
||||||
|
fireEvent(window, new MouseEvent('mousemove', { bubbles: true }));
|
||||||
|
expect(reset).toHaveBeenCalled();
|
||||||
|
|
||||||
|
fireEvent(window, new MouseEvent('mouseup', { bubbles: true }));
|
||||||
|
|
||||||
|
await waitFor(() =>
|
||||||
|
expect(trackSpy).toHaveBeenCalledWith(
|
||||||
|
'User dragged data for mapping',
|
||||||
|
expect.any(Object),
|
||||||
|
expect.any(Object),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should expand all nodes when searching', async () => {
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode1,
|
||||||
|
data: [{ json: { name: 'John' } }],
|
||||||
|
});
|
||||||
|
useWorkflowsStore().pinData({
|
||||||
|
node: mockNode2,
|
||||||
|
data: [{ json: { name: 'John' } }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getAllByTestId, queryAllByTestId, rerender } = renderComponent();
|
||||||
|
const headers = getAllByTestId('run-data-schema-header');
|
||||||
|
expect(headers.length).toBe(2);
|
||||||
|
expect(getAllByTestId('run-data-schema-item').length).toBe(2);
|
||||||
|
|
||||||
|
// Collapse all nodes
|
||||||
|
await Promise.all(headers.map(async ($header) => await userEvent.click($header)));
|
||||||
|
|
||||||
|
expect(queryAllByTestId('run-data-schema-item').length).toBe(0);
|
||||||
|
await rerender({ search: 'John' });
|
||||||
|
expect(getAllByTestId('run-data-schema-item').length).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
292
packages/editor-ui/src/components/VirtualSchema.vue
Normal file
292
packages/editor-ui/src/components/VirtualSchema.vue
Normal file
|
@ -0,0 +1,292 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, watch, ref } from 'vue';
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import VirtualSchemaItem from '@/components/VirtualSchemaItem.vue';
|
||||||
|
import VirtualSchemaHeader from '@/components/VirtualSchemaHeader.vue';
|
||||||
|
import { N8nText } from 'n8n-design-system';
|
||||||
|
import Draggable from '@/components/Draggable.vue';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { NodeConnectionType, type IConnectedNode, type IDataObject } from 'n8n-workflow';
|
||||||
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import MappingPill from './MappingPill.vue';
|
||||||
|
import { useDataSchema, useFlattenSchema, type SchemaNode } from '@/composables/useDataSchema';
|
||||||
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
|
import { executionDataToJson } from '@/utils/nodeTypesUtils';
|
||||||
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
|
import {
|
||||||
|
DynamicScroller,
|
||||||
|
DynamicScrollerItem,
|
||||||
|
type RecycleScrollerInstance,
|
||||||
|
} from 'vue-virtual-scroller';
|
||||||
|
|
||||||
|
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
nodes?: IConnectedNode[];
|
||||||
|
node?: INodeUi | null;
|
||||||
|
data?: IDataObject[];
|
||||||
|
mappingEnabled?: boolean;
|
||||||
|
runIndex?: number;
|
||||||
|
outputIndex?: number;
|
||||||
|
totalRuns?: number;
|
||||||
|
paneType: 'input' | 'output';
|
||||||
|
connectionType?: NodeConnectionType;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
nodes: () => [],
|
||||||
|
distanceFromActive: 1,
|
||||||
|
node: null,
|
||||||
|
data: () => [],
|
||||||
|
runIndex: 0,
|
||||||
|
outputIndex: 0,
|
||||||
|
totalRuns: 1,
|
||||||
|
connectionType: NodeConnectionType.Main,
|
||||||
|
search: '',
|
||||||
|
mappingEnabled: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
const i18n = useI18n();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const nodeTypesStore = useNodeTypesStore();
|
||||||
|
const workflowsStore = useWorkflowsStore();
|
||||||
|
const { getSchemaForExecutionData, filterSchema } = useDataSchema();
|
||||||
|
const { closedNodes, flattenSchema, flattenMultipleSchemas, toggleLeaf, toggleNode } =
|
||||||
|
useFlattenSchema();
|
||||||
|
const { getNodeInputData } = useNodeHelpers();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'clear:search': [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const scroller = ref<RecycleScrollerInstance>();
|
||||||
|
|
||||||
|
const toggleNodeAndScrollTop = (id: string) => {
|
||||||
|
toggleNode(id);
|
||||||
|
scroller.value?.scrollToItem(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.search,
|
||||||
|
(newSearch) => {
|
||||||
|
if (!newSearch) return;
|
||||||
|
closedNodes.value.clear();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const getNodeSchema = (fullNode: INodeUi, connectedNode: IConnectedNode) => {
|
||||||
|
const pinData = workflowsStore.pinDataByNodeName(connectedNode.name);
|
||||||
|
const connectedOutputIndexes = connectedNode.indicies.length > 0 ? connectedNode.indicies : [0];
|
||||||
|
const data =
|
||||||
|
pinData ??
|
||||||
|
connectedOutputIndexes
|
||||||
|
.map((outputIndex) =>
|
||||||
|
executionDataToJson(
|
||||||
|
getNodeInputData(
|
||||||
|
fullNode,
|
||||||
|
props.runIndex,
|
||||||
|
outputIndex,
|
||||||
|
props.paneType,
|
||||||
|
props.connectionType,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
return {
|
||||||
|
schema: getSchemaForExecutionData(data),
|
||||||
|
connectedOutputIndexes,
|
||||||
|
itemsCount: data.length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const nodeSchema = computed(() =>
|
||||||
|
filterSchema(getSchemaForExecutionData(props.data), props.search),
|
||||||
|
);
|
||||||
|
|
||||||
|
const nodesSchemas = computed<SchemaNode[]>(() => {
|
||||||
|
return props.nodes.reduce<SchemaNode[]>((acc, node) => {
|
||||||
|
const fullNode = workflowsStore.getNodeByName(node.name);
|
||||||
|
if (!fullNode) return acc;
|
||||||
|
|
||||||
|
const nodeType = nodeTypesStore.getNodeType(fullNode.type, fullNode.typeVersion);
|
||||||
|
if (!nodeType) return acc;
|
||||||
|
|
||||||
|
const { schema, connectedOutputIndexes, itemsCount } = getNodeSchema(fullNode, node);
|
||||||
|
|
||||||
|
const filteredSchema = filterSchema(schema, props.search);
|
||||||
|
|
||||||
|
if (!filteredSchema) return acc;
|
||||||
|
|
||||||
|
acc.push({
|
||||||
|
node: fullNode,
|
||||||
|
connectedOutputIndexes,
|
||||||
|
depth: node.depth,
|
||||||
|
itemsCount,
|
||||||
|
nodeType,
|
||||||
|
schema: filteredSchema,
|
||||||
|
});
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
const nodeAdditionalInfo = (node: INodeUi) => {
|
||||||
|
const returnData: string[] = [];
|
||||||
|
if (node.disabled) {
|
||||||
|
returnData.push(i18n.baseText('node.disabled'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const connections = ndvStore.ndvNodeInputNumber[node.name];
|
||||||
|
if (connections) {
|
||||||
|
if (connections.length === 1) {
|
||||||
|
returnData.push(`Input ${connections}`);
|
||||||
|
} else {
|
||||||
|
returnData.push(`Inputs ${connections.join(', ')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData.length ? `(${returnData.join(' | ')})` : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenedNodes = computed(() =>
|
||||||
|
flattenMultipleSchemas(nodesSchemas.value, nodeAdditionalInfo),
|
||||||
|
);
|
||||||
|
|
||||||
|
const flattenNodeSchema = computed(() =>
|
||||||
|
nodeSchema.value ? flattenSchema({ schema: nodeSchema.value, depth: 0, level: -1 }) : [],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In debug mode nodes are empty
|
||||||
|
*/
|
||||||
|
const isDebugging = computed(() => !props.nodes.length);
|
||||||
|
|
||||||
|
const items = computed(() => {
|
||||||
|
if (isDebugging.value || props.paneType === 'output') {
|
||||||
|
return flattenNodeSchema.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return flattenedNodes.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const noSearchResults = computed(() => {
|
||||||
|
return Boolean(props.search.trim()) && !Boolean(items.value.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
const onDragStart = () => {
|
||||||
|
ndvStore.resetMappingTelemetry();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDragEnd = (el: HTMLElement) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
const mappingTelemetry = ndvStore.mappingTelemetry;
|
||||||
|
const telemetryPayload = {
|
||||||
|
src_node_type: el.dataset.nodeType,
|
||||||
|
src_field_name: el.dataset.name ?? '',
|
||||||
|
src_nodes_back: el.dataset.depth,
|
||||||
|
src_run_index: props.runIndex,
|
||||||
|
src_runs_total: props.totalRuns,
|
||||||
|
src_field_nest_level: el.dataset.level ?? 0,
|
||||||
|
src_view: 'schema',
|
||||||
|
src_element: el,
|
||||||
|
success: false,
|
||||||
|
...mappingTelemetry,
|
||||||
|
};
|
||||||
|
|
||||||
|
void useExternalHooks().run('runDataJson.onDragEnd', telemetryPayload);
|
||||||
|
|
||||||
|
telemetry.track('User dragged data for mapping', telemetryPayload, { withPostHog: true });
|
||||||
|
}, 250); // ensure dest data gets set if drop
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="run-data-schema full-height">
|
||||||
|
<div v-if="noSearchResults" class="no-results">
|
||||||
|
<N8nText tag="h3" size="large">{{ i18n.baseText('ndv.search.noNodeMatch.title') }}</N8nText>
|
||||||
|
<N8nText>
|
||||||
|
<i18n-t keypath="ndv.search.noMatch.description" tag="span">
|
||||||
|
<template #link>
|
||||||
|
<a href="#" @click="emit('clear:search')">
|
||||||
|
{{ i18n.baseText('ndv.search.noMatch.description.link') }}
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
</i18n-t>
|
||||||
|
</N8nText>
|
||||||
|
|
||||||
|
<N8nText v-if="paneType === 'output'">
|
||||||
|
{{ i18n.baseText('ndv.search.noMatchSchema.description') }}
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Draggable
|
||||||
|
v-if="items.length"
|
||||||
|
class="full-height"
|
||||||
|
type="mapping"
|
||||||
|
target-data-key="mappable"
|
||||||
|
:disabled="!mappingEnabled"
|
||||||
|
@dragstart="onDragStart"
|
||||||
|
@dragend="onDragEnd"
|
||||||
|
>
|
||||||
|
<template #preview="{ canDrop, el }">
|
||||||
|
<MappingPill v-if="el" :html="el.outerHTML" :can-drop="canDrop" />
|
||||||
|
</template>
|
||||||
|
<DynamicScroller
|
||||||
|
ref="scroller"
|
||||||
|
:items="items"
|
||||||
|
:min-item-size="38"
|
||||||
|
class="full-height scroller"
|
||||||
|
>
|
||||||
|
<template #default="{ item, index, active }">
|
||||||
|
<VirtualSchemaHeader
|
||||||
|
v-if="item.type === 'header'"
|
||||||
|
v-bind="item"
|
||||||
|
:collapsed="closedNodes.has(item.id)"
|
||||||
|
@click:toggle="toggleLeaf(item.id)"
|
||||||
|
@click="toggleNodeAndScrollTop(item.id)"
|
||||||
|
/>
|
||||||
|
<DynamicScrollerItem
|
||||||
|
v-else
|
||||||
|
:item="item"
|
||||||
|
:active="active"
|
||||||
|
:size-dependencies="[item.value]"
|
||||||
|
:data-index="index"
|
||||||
|
>
|
||||||
|
<VirtualSchemaItem
|
||||||
|
v-bind="item"
|
||||||
|
:search="search"
|
||||||
|
:draggable="mappingEnabled"
|
||||||
|
:collapsed="closedNodes.has(item.id)"
|
||||||
|
:highlight="ndvStore.highlightDraggables"
|
||||||
|
@click="toggleLeaf(item.id)"
|
||||||
|
>
|
||||||
|
</VirtualSchemaItem>
|
||||||
|
</DynamicScrollerItem>
|
||||||
|
</template>
|
||||||
|
</DynamicScroller>
|
||||||
|
</Draggable>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.full-height {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.run-data-schema {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scroller {
|
||||||
|
padding: 0 var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-s) var(--spacing-s) var(--spacing-xl) var(--spacing-s);
|
||||||
|
}
|
||||||
|
</style>
|
104
packages/editor-ui/src/components/VirtualSchemaHeader.vue
Normal file
104
packages/editor-ui/src/components/VirtualSchemaHeader.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
import { type INodeTypeDescription } from 'n8n-workflow';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
info?: string;
|
||||||
|
collapsable: boolean;
|
||||||
|
collapsed: boolean;
|
||||||
|
nodeType: INodeTypeDescription;
|
||||||
|
itemCount: number | null;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const isTrigger = computed(() => props.nodeType.group.includes('trigger'));
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'click:toggle': [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schema-header" data-test-id="run-data-schema-header">
|
||||||
|
<div class="toggle" @click.capture.stop="emit('click:toggle')">
|
||||||
|
<FontAwesomeIcon icon="angle-down" :class="{ 'collapse-icon': true, collapsed }" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<NodeIcon
|
||||||
|
class="icon"
|
||||||
|
:class="{ ['icon-trigger']: isTrigger }"
|
||||||
|
:node-type="nodeType"
|
||||||
|
:size="12"
|
||||||
|
/>
|
||||||
|
<div class="title">
|
||||||
|
{{ title }}
|
||||||
|
<span v-if="info" class="info">{{ info }}</span>
|
||||||
|
</div>
|
||||||
|
<FontAwesomeIcon v-if="isTrigger" class="trigger-icon" icon="bolt" size="xs" />
|
||||||
|
<div v-if="itemCount" class="item-count" data-test-id="run-data-schema-node-item-count">
|
||||||
|
{{ i18n.baseText('ndv.output.items', { interpolate: { count: itemCount } }) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.schema-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-bottom: var(--spacing-2xs);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.toggle {
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.collapse-icon {
|
||||||
|
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
}
|
||||||
|
.collapsed {
|
||||||
|
transform: rotateZ(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: var(--spacing-3xs);
|
||||||
|
border: 1px solid var(--color-foreground-light);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
margin-right: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-trigger {
|
||||||
|
border-radius: 16px 4px 4px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trigger-icon {
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-count {
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
142
packages/editor-ui/src/components/VirtualSchemaItem.vue
Normal file
142
packages/editor-ui/src/components/VirtualSchemaItem.vue
Normal file
|
@ -0,0 +1,142 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import TextWithHighlights from './TextWithHighlights.vue';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title?: string;
|
||||||
|
path?: string;
|
||||||
|
level?: number;
|
||||||
|
depth?: number;
|
||||||
|
expression?: string;
|
||||||
|
value?: string;
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
collapsable?: boolean;
|
||||||
|
nodeType?: string;
|
||||||
|
highlight?: boolean;
|
||||||
|
draggable?: boolean;
|
||||||
|
collapsed?: boolean;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
click: [];
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="schema-item" :class="{ draggable }" data-test-id="run-data-schema-item">
|
||||||
|
<div class="toggle-container">
|
||||||
|
<div v-if="collapsable" class="toggle" @click="emit('click')">
|
||||||
|
<FontAwesomeIcon icon="angle-down" :class="{ 'collapse-icon': true, collapsed }" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="title"
|
||||||
|
:data-name="title"
|
||||||
|
:data-path="path"
|
||||||
|
:data-depth="depth"
|
||||||
|
:data-nest-level="level"
|
||||||
|
:data-value="expression"
|
||||||
|
:data-node-type="nodeType"
|
||||||
|
data-target="mappable"
|
||||||
|
class="pill"
|
||||||
|
:class="{ 'pill--highlight': highlight }"
|
||||||
|
data-test-id="run-data-schema-node-name"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon class="type-icon" :icon size="sm" />
|
||||||
|
<TextWithHighlights class="title" :content="title" :search="props.search" />
|
||||||
|
</div>
|
||||||
|
<TextWithHighlights
|
||||||
|
data-test-id="run-data-schema-item-value"
|
||||||
|
class="text"
|
||||||
|
:content="value"
|
||||||
|
:search="props.search"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped>
|
||||||
|
.schema-item {
|
||||||
|
display: flex;
|
||||||
|
margin-left: calc(var(--spacing-l) * v-bind(level));
|
||||||
|
align-items: baseline;
|
||||||
|
padding-bottom: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-container {
|
||||||
|
min-width: var(--spacing-l);
|
||||||
|
min-height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pill {
|
||||||
|
display: inline-flex;
|
||||||
|
height: 24px;
|
||||||
|
padding: 0 var(--spacing-3xs);
|
||||||
|
border: 1px solid var(--color-foreground-light);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
background-color: var(--color-background-xlight);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
max-width: 50%;
|
||||||
|
align-items: center;
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin-left: var(--spacing-3xs);
|
||||||
|
padding-left: var(--spacing-3xs);
|
||||||
|
border-left: 1px solid var(--color-foreground-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill.pill--highlight {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-color: var(--color-primary-tint-1);
|
||||||
|
background-color: var(--color-primary-tint-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill.pill--highlight .type-icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill {
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable .pill:hover {
|
||||||
|
background-color: var(--color-background-light);
|
||||||
|
border-color: var(--color-foreground-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.type-icon {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
margin-left: var(--spacing-2xs);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-icon {
|
||||||
|
transition: transform 0.2s cubic-bezier(0.19, 1, 0.22, 1);
|
||||||
|
}
|
||||||
|
.collapsed {
|
||||||
|
transform: rotateZ(-90deg);
|
||||||
|
}
|
||||||
|
</style>
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
import jp from 'jsonpath';
|
import jp from 'jsonpath';
|
||||||
import { useDataSchema } from '@/composables/useDataSchema';
|
import { useDataSchema, useFlattenSchema } 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';
|
||||||
|
@ -649,3 +649,39 @@ describe('useDataSchema', () => {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useFlattenSchema', () => {
|
||||||
|
it('flattens a schema', () => {
|
||||||
|
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',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
useFlattenSchema().flattenSchema({
|
||||||
|
schema,
|
||||||
|
}).length,
|
||||||
|
).toBe(3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,15 +1,18 @@
|
||||||
|
import { ref } from 'vue';
|
||||||
import type { Optional, Primitives, Schema, INodeUi } from '@/Interface';
|
import type { Optional, Primitives, Schema, INodeUi } from '@/Interface';
|
||||||
import {
|
import {
|
||||||
type ITaskDataConnections,
|
type ITaskDataConnections,
|
||||||
type IDataObject,
|
type IDataObject,
|
||||||
type INodeExecutionData,
|
type INodeExecutionData,
|
||||||
|
type INodeTypeDescription,
|
||||||
NodeConnectionType,
|
NodeConnectionType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { merge } from 'lodash-es';
|
import { merge } from 'lodash-es';
|
||||||
import { generatePath } from '@/utils/mappingUtils';
|
import { generatePath, getMappedExpression } from '@/utils/mappingUtils';
|
||||||
import { isObj } from '@/utils/typeGuards';
|
import { isObj } from '@/utils/typeGuards';
|
||||||
import { useWorkflowsStore } from '@/stores/workflows.store';
|
import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { isPresent } from '@/utils/typesUtils';
|
import { isPresent, shorten } from '@/utils/typesUtils';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
export function useDataSchema() {
|
export function useDataSchema() {
|
||||||
function getSchema(
|
function getSchema(
|
||||||
|
@ -166,3 +169,208 @@ export function useDataSchema() {
|
||||||
filterSchema,
|
filterSchema,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SchemaNode = {
|
||||||
|
node: INodeUi;
|
||||||
|
nodeType: INodeTypeDescription;
|
||||||
|
depth: number;
|
||||||
|
connectedOutputIndexes: number[];
|
||||||
|
itemsCount: number;
|
||||||
|
schema: Schema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderItem = {
|
||||||
|
title?: string;
|
||||||
|
path?: string;
|
||||||
|
level?: number;
|
||||||
|
depth?: number;
|
||||||
|
expression?: string;
|
||||||
|
value?: string;
|
||||||
|
id: string;
|
||||||
|
icon: string;
|
||||||
|
collapsable?: boolean;
|
||||||
|
nodeType?: INodeUi['type'];
|
||||||
|
type: 'item';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RenderHeader = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
info?: string;
|
||||||
|
collapsable: boolean;
|
||||||
|
nodeType: INodeTypeDescription;
|
||||||
|
itemCount: number | null;
|
||||||
|
type: 'header';
|
||||||
|
};
|
||||||
|
|
||||||
|
type Renders = RenderHeader | RenderItem;
|
||||||
|
|
||||||
|
const icons = {
|
||||||
|
object: 'cube',
|
||||||
|
array: 'list',
|
||||||
|
['string']: 'font',
|
||||||
|
null: 'font',
|
||||||
|
['number']: 'hashtag',
|
||||||
|
['boolean']: 'check-square',
|
||||||
|
function: 'code',
|
||||||
|
bigint: 'calculator',
|
||||||
|
symbol: 'sun',
|
||||||
|
['undefined']: 'ban',
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const getIconBySchemaType = (type: Schema['type']): string => icons[type];
|
||||||
|
|
||||||
|
const emptyItem = (): RenderItem => ({
|
||||||
|
id: `empty-${window.crypto.randomUUID()}`,
|
||||||
|
icon: '',
|
||||||
|
value: useI18n().baseText('dataMapping.schemaView.emptyData'),
|
||||||
|
type: 'item',
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDataEmpty = (schema: Schema) => {
|
||||||
|
// Utilize the generated schema instead of looping over the entire data again
|
||||||
|
// The schema for empty data is { type: 'object', value: [] }
|
||||||
|
const isObjectOrArray = schema.type === 'object';
|
||||||
|
const isEmpty = Array.isArray(schema.value) && schema.value.length === 0;
|
||||||
|
|
||||||
|
return isObjectOrArray && isEmpty;
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefixTitle = (title: string, prefix?: string) => (prefix ? `${prefix}[${title}]` : title);
|
||||||
|
|
||||||
|
export const useFlattenSchema = () => {
|
||||||
|
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) => {
|
||||||
|
if (closedNodes.value.has(id)) {
|
||||||
|
closedNodes.value = new Set(headerIds.value);
|
||||||
|
closedNodes.value.delete(id);
|
||||||
|
} else {
|
||||||
|
closedNodes.value.add(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenSchema = ({
|
||||||
|
schema,
|
||||||
|
node = { name: '', type: '' },
|
||||||
|
depth = 0,
|
||||||
|
prefix = '',
|
||||||
|
level = 0,
|
||||||
|
}: {
|
||||||
|
schema: Schema;
|
||||||
|
node?: { name: string; type: string };
|
||||||
|
depth?: number;
|
||||||
|
prefix?: string;
|
||||||
|
level?: number;
|
||||||
|
}): RenderItem[] => {
|
||||||
|
// only show empty item for the first level
|
||||||
|
if (isDataEmpty(schema) && depth <= 0) {
|
||||||
|
return [emptyItem()];
|
||||||
|
}
|
||||||
|
|
||||||
|
const expression = getMappedExpression({
|
||||||
|
nodeName: node.name,
|
||||||
|
distanceFromActive: depth,
|
||||||
|
path: schema.path,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(schema.value)) {
|
||||||
|
const items: RenderItem[] = [];
|
||||||
|
|
||||||
|
if (schema.key) {
|
||||||
|
items.push({
|
||||||
|
title: prefixTitle(schema.key, prefix),
|
||||||
|
path: schema.path,
|
||||||
|
expression,
|
||||||
|
depth,
|
||||||
|
level,
|
||||||
|
icon: getIconBySchemaType(schema.type),
|
||||||
|
id: expression,
|
||||||
|
collapsable: true,
|
||||||
|
nodeType: node.type,
|
||||||
|
type: 'item',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closedNodes.value.has(expression)) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
return items.concat(
|
||||||
|
schema.value
|
||||||
|
.map((item) => {
|
||||||
|
const itemPrefix = schema.type === 'array' ? schema.key : '';
|
||||||
|
return flattenSchema({
|
||||||
|
schema: item,
|
||||||
|
node,
|
||||||
|
depth,
|
||||||
|
prefix: itemPrefix,
|
||||||
|
level: level + 1,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.flat(),
|
||||||
|
);
|
||||||
|
} else if (schema.key) {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
title: prefixTitle(schema.key, prefix),
|
||||||
|
path: schema.path,
|
||||||
|
expression,
|
||||||
|
level,
|
||||||
|
depth,
|
||||||
|
value: shorten(schema.value, 600, 0),
|
||||||
|
id: expression,
|
||||||
|
icon: getIconBySchemaType(schema.type),
|
||||||
|
collapsable: false,
|
||||||
|
nodeType: node.type,
|
||||||
|
type: 'item',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
const flattenMultipleSchemas = (
|
||||||
|
nodes: SchemaNode[],
|
||||||
|
additionalInfo: (node: INodeUi) => string,
|
||||||
|
) => {
|
||||||
|
headerIds.value.clear();
|
||||||
|
|
||||||
|
return nodes.reduce<Renders[]>((acc, item) => {
|
||||||
|
acc.push({
|
||||||
|
title: item.node.name,
|
||||||
|
id: item.node.name,
|
||||||
|
collapsable: true,
|
||||||
|
nodeType: item.nodeType,
|
||||||
|
itemCount: item.itemsCount,
|
||||||
|
info: additionalInfo(item.node),
|
||||||
|
type: 'header',
|
||||||
|
});
|
||||||
|
|
||||||
|
headerIds.value.add(item.node.name);
|
||||||
|
|
||||||
|
if (closedNodes.value.has(item.node.name)) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDataEmpty(item.schema)) {
|
||||||
|
acc.push(emptyItem());
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc.push(...flattenSchema(item));
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
return { closedNodes, toggleLeaf, toggleNode, flattenSchema, flattenMultipleSchemas };
|
||||||
|
};
|
||||||
|
|
112
packages/editor-ui/src/vue-virtual-scroller.d.ts
vendored
Normal file
112
packages/editor-ui/src/vue-virtual-scroller.d.ts
vendored
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
declare module 'vue-virtual-scroller' {
|
||||||
|
import {
|
||||||
|
type ObjectEmitsOptions,
|
||||||
|
type PublicProps,
|
||||||
|
type SetupContext,
|
||||||
|
type SlotsType,
|
||||||
|
type VNode,
|
||||||
|
} from 'vue';
|
||||||
|
|
||||||
|
interface RecycleScrollerProps<T> {
|
||||||
|
items: readonly T[];
|
||||||
|
direction?: 'vertical' | 'horizontal';
|
||||||
|
itemSize?: number | null;
|
||||||
|
gridItems?: number;
|
||||||
|
itemSecondarySize?: number;
|
||||||
|
minItemSize?: number;
|
||||||
|
sizeField?: string;
|
||||||
|
typeField?: string;
|
||||||
|
keyField?: keyof T;
|
||||||
|
pageMode?: boolean;
|
||||||
|
prerender?: number;
|
||||||
|
buffer?: number;
|
||||||
|
emitUpdate?: boolean;
|
||||||
|
updateInterval?: number;
|
||||||
|
listClass?: string;
|
||||||
|
itemClass?: string;
|
||||||
|
listTag?: string;
|
||||||
|
itemTag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DynamicScrollerProps<T> extends RecycleScrollerProps<T> {
|
||||||
|
minItemSize: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecycleScrollerEmitOptions extends ObjectEmitsOptions {
|
||||||
|
resize: () => void;
|
||||||
|
visible: () => void;
|
||||||
|
hidden: () => void;
|
||||||
|
update: (
|
||||||
|
startIndex: number,
|
||||||
|
endIndex: number,
|
||||||
|
visibleStartIndex: number,
|
||||||
|
visibleEndIndex: number,
|
||||||
|
) => void;
|
||||||
|
'scroll-start': () => void;
|
||||||
|
'scroll-end': () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecycleScrollerSlotProps<T> {
|
||||||
|
item: T;
|
||||||
|
index: number;
|
||||||
|
active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface RecycleScrollerSlots<T> {
|
||||||
|
default(slotProps: RecycleScrollerSlotProps<T>): unknown;
|
||||||
|
before(): unknown;
|
||||||
|
empty(): unknown;
|
||||||
|
after(): unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RecycleScrollerInstance {
|
||||||
|
getScroll(): { start: number; end: number };
|
||||||
|
scrollToItem(index: number): void;
|
||||||
|
scrollToPosition(position: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RecycleScroller: <T>(
|
||||||
|
props: RecycleScrollerProps<T> & PublicProps,
|
||||||
|
ctx?: SetupContext<RecycleScrollerEmitOptions, SlotsType<RecycleScrollerSlots<T>>>,
|
||||||
|
expose?: (exposed: RecycleScrollerInstance) => void,
|
||||||
|
) => VNode & {
|
||||||
|
__ctx?: {
|
||||||
|
props: RecycleScrollerProps<T> & PublicProps;
|
||||||
|
expose(exposed: RecycleScrollerInstance): void;
|
||||||
|
slots: RecycleScrollerSlots<T>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DynamicScroller: <T>(
|
||||||
|
props: DynamicScrollerProps<T> & PublicProps,
|
||||||
|
ctx?: SetupContext<RecycleScrollerEmitOptions, SlotsType<RecycleScrollerSlots<T>>>,
|
||||||
|
expose?: (exposed: RecycleScrollerInstance) => void,
|
||||||
|
) => VNode & {
|
||||||
|
__ctx?: {
|
||||||
|
props: DynamicScrollerProps<T> & PublicProps;
|
||||||
|
expose(exposed: RecycleScrollerInstance): void;
|
||||||
|
slots: RecycleScrollerSlots<T>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DynamicScrollerItemProps<T> {
|
||||||
|
item: T;
|
||||||
|
active: boolean;
|
||||||
|
sizeDependencies?: unknown[];
|
||||||
|
watchData?: boolean;
|
||||||
|
tag?: string;
|
||||||
|
emitResize?: boolean;
|
||||||
|
onResize?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DynamicScrollerItemEmitOptions extends ObjectEmitsOptions {
|
||||||
|
resize: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DynamicScrollerItem: <T>(
|
||||||
|
props: DynamicScrollerItemProps<T> & PublicProps,
|
||||||
|
ctx?: SetupContext<DynamicScrollerItemEmitOptions>,
|
||||||
|
) => VNode;
|
||||||
|
|
||||||
|
export function IdState(options?: { idProp?: (value: any) => unknown }): unknown;
|
||||||
|
}
|
140
pnpm-lock.yaml
140
pnpm-lock.yaml
|
@ -1114,7 +1114,7 @@ importers:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core':
|
'@langchain/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.15(openai@4.69.0(zod@3.23.8))
|
version: 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
|
||||||
'@n8n/client-oauth2':
|
'@n8n/client-oauth2':
|
||||||
specifier: workspace:*
|
specifier: workspace:*
|
||||||
version: link:../@n8n/client-oauth2
|
version: link:../@n8n/client-oauth2
|
||||||
|
@ -1513,6 +1513,9 @@ importers:
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: catalog:frontend
|
specifier: catalog:frontend
|
||||||
version: 4.4.5(vue@3.5.11(typescript@5.7.2))
|
version: 4.4.5(vue@3.5.11(typescript@5.7.2))
|
||||||
|
vue-virtual-scroller:
|
||||||
|
specifier: 2.0.0-beta.8
|
||||||
|
version: 2.0.0-beta.8(vue@3.5.11(typescript@5.7.2))
|
||||||
vue3-touch-events:
|
vue3-touch-events:
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.3
|
version: 4.1.3
|
||||||
|
@ -1942,7 +1945,7 @@ importers:
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@langchain/core':
|
'@langchain/core':
|
||||||
specifier: 'catalog:'
|
specifier: 'catalog:'
|
||||||
version: 0.3.15(openai@4.69.0)
|
version: 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
|
||||||
'@types/deep-equal':
|
'@types/deep-equal':
|
||||||
specifier: ^1.0.1
|
specifier: ^1.0.1
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
|
@ -9244,6 +9247,9 @@ packages:
|
||||||
resolution: {integrity: sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==}
|
resolution: {integrity: sha512-MGZAq0Q3OuRYgZKvlB69z4gLN4G3PvgC4A2zhkCXCXrLD5wm2cCnwNB59xOBVA+srZ0zEes6u+VylcPIkB4SqA==}
|
||||||
engines: {node: 6.* || 8.* || >= 10.*}
|
engines: {node: 6.* || 8.* || >= 10.*}
|
||||||
|
|
||||||
|
mitt@2.1.0:
|
||||||
|
resolution: {integrity: sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==}
|
||||||
|
|
||||||
mjml-accordion@4.15.3:
|
mjml-accordion@4.15.3:
|
||||||
resolution: {integrity: sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==}
|
resolution: {integrity: sha512-LPNVSj1LyUVYT9G1gWwSw3GSuDzDsQCu0tPB2uDsq4VesYNnU6v3iLCQidMiR6azmIt13OEozG700ygAUuA6Ng==}
|
||||||
|
|
||||||
|
@ -11004,10 +11010,6 @@ packages:
|
||||||
resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==}
|
resolution: {integrity: sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==}
|
||||||
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
engines: {node: '>= 10.0.0', npm: '>= 3.0.0'}
|
||||||
|
|
||||||
source-map-js@1.0.2:
|
|
||||||
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
|
|
||||||
engines: {node: '>=0.10.0'}
|
|
||||||
|
|
||||||
source-map-js@1.2.0:
|
source-map-js@1.2.0:
|
||||||
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
@ -12134,6 +12136,16 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.3.4
|
vue: ^3.3.4
|
||||||
|
|
||||||
|
vue-observe-visibility@2.0.0-alpha.1:
|
||||||
|
resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.0.0
|
||||||
|
|
||||||
|
vue-resize@2.0.0-alpha.1:
|
||||||
|
resolution: {integrity: sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.0.0
|
||||||
|
|
||||||
vue-router@4.4.5:
|
vue-router@4.4.5:
|
||||||
resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==}
|
resolution: {integrity: sha512-4fKZygS8cH1yCyuabAXGUAsyi1b2/o/OKgu/RUb+znIYOxPRxdkytJEx+0wGcpBE1pX6vUgh5jwWOKRGvuA/7Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -12145,6 +12157,11 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
typescript: ^5.7.2
|
typescript: ^5.7.2
|
||||||
|
|
||||||
|
vue-virtual-scroller@2.0.0-beta.8:
|
||||||
|
resolution: {integrity: sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: ^3.2.0
|
||||||
|
|
||||||
vue3-touch-events@4.1.3:
|
vue3-touch-events@4.1.3:
|
||||||
resolution: {integrity: sha512-uXTclRzn7de1mgiDIZ8N4J/wnWl1vBPLTWr60fqoLXu7ifhDKpl83Q2m9qA20KfEiAy+L4X/xXGc5ptGmdPh4A==}
|
resolution: {integrity: sha512-uXTclRzn7de1mgiDIZ8N4J/wnWl1vBPLTWr60fqoLXu7ifhDKpl83Q2m9qA20KfEiAy+L4X/xXGc5ptGmdPh4A==}
|
||||||
|
|
||||||
|
@ -14667,38 +14684,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- openai
|
- openai
|
||||||
|
|
||||||
'@langchain/core@0.3.15(openai@4.69.0(zod@3.23.8))':
|
|
||||||
dependencies:
|
|
||||||
ansi-styles: 5.2.0
|
|
||||||
camelcase: 6.3.0
|
|
||||||
decamelize: 1.2.0
|
|
||||||
js-tiktoken: 1.0.12
|
|
||||||
langsmith: 0.2.3(openai@4.69.0(zod@3.23.8))
|
|
||||||
mustache: 4.2.0
|
|
||||||
p-queue: 6.6.2
|
|
||||||
p-retry: 4.6.2
|
|
||||||
uuid: 10.0.0
|
|
||||||
zod: 3.23.8
|
|
||||||
zod-to-json-schema: 3.23.3(zod@3.23.8)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- openai
|
|
||||||
|
|
||||||
'@langchain/core@0.3.15(openai@4.69.0)':
|
|
||||||
dependencies:
|
|
||||||
ansi-styles: 5.2.0
|
|
||||||
camelcase: 6.3.0
|
|
||||||
decamelize: 1.2.0
|
|
||||||
js-tiktoken: 1.0.12
|
|
||||||
langsmith: 0.2.3(openai@4.69.0)
|
|
||||||
mustache: 4.2.0
|
|
||||||
p-queue: 6.6.2
|
|
||||||
p-retry: 4.6.2
|
|
||||||
uuid: 10.0.0
|
|
||||||
zod: 3.23.8
|
|
||||||
zod-to-json-schema: 3.23.3(zod@3.23.8)
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- openai
|
|
||||||
|
|
||||||
'@langchain/google-common@0.1.1(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)':
|
'@langchain/google-common@0.1.1(@langchain/core@0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8)))(zod@3.23.8)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
|
'@langchain/core': 0.3.15(openai@4.69.0(encoding@0.1.13)(zod@3.23.8))
|
||||||
|
@ -19264,7 +19249,7 @@ snapshots:
|
||||||
|
|
||||||
eslint-import-resolver-node@0.3.9:
|
eslint-import-resolver-node@0.3.9:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
is-core-module: 2.13.1
|
is-core-module: 2.13.1
|
||||||
resolve: 1.22.8
|
resolve: 1.22.8
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
|
@ -19289,7 +19274,7 @@ snapshots:
|
||||||
|
|
||||||
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
|
eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-plugin-import@2.29.1)(eslint@8.57.0))(eslint@8.57.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2)
|
'@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2)
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
|
@ -19309,7 +19294,7 @@ snapshots:
|
||||||
array.prototype.findlastindex: 1.2.3
|
array.prototype.findlastindex: 1.2.3
|
||||||
array.prototype.flat: 1.3.2
|
array.prototype.flat: 1.3.2
|
||||||
array.prototype.flatmap: 1.3.2
|
array.prototype.flatmap: 1.3.2
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
doctrine: 2.1.0
|
doctrine: 2.1.0
|
||||||
eslint: 8.57.0
|
eslint: 8.57.0
|
||||||
eslint-import-resolver-node: 0.3.9
|
eslint-import-resolver-node: 0.3.9
|
||||||
|
@ -20102,7 +20087,7 @@ snapshots:
|
||||||
array-parallel: 0.1.3
|
array-parallel: 0.1.3
|
||||||
array-series: 0.1.5
|
array-series: 0.1.5
|
||||||
cross-spawn: 4.0.2
|
cross-spawn: 4.0.2
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -20430,7 +20415,7 @@ snapshots:
|
||||||
isstream: 0.1.2
|
isstream: 0.1.2
|
||||||
jsonwebtoken: 9.0.2
|
jsonwebtoken: 9.0.2
|
||||||
mime-types: 2.1.35
|
mime-types: 2.1.35
|
||||||
retry-axios: 2.6.0(axios@1.7.4(debug@4.3.7))
|
retry-axios: 2.6.0(axios@1.7.4)
|
||||||
tough-cookie: 4.1.3
|
tough-cookie: 4.1.3
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -21272,7 +21257,7 @@ snapshots:
|
||||||
form-data: 4.0.0
|
form-data: 4.0.0
|
||||||
html-encoding-sniffer: 4.0.0
|
html-encoding-sniffer: 4.0.0
|
||||||
http-proxy-agent: 7.0.0
|
http-proxy-agent: 7.0.0
|
||||||
https-proxy-agent: 7.0.2
|
https-proxy-agent: 7.0.5
|
||||||
is-potential-custom-element-name: 1.0.1
|
is-potential-custom-element-name: 1.0.1
|
||||||
nwsapi: 2.2.7
|
nwsapi: 2.2.7
|
||||||
parse5: 7.1.2
|
parse5: 7.1.2
|
||||||
|
@ -21467,28 +21452,6 @@ snapshots:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
openai: 4.69.0(encoding@0.1.13)(zod@3.23.8)
|
openai: 4.69.0(encoding@0.1.13)(zod@3.23.8)
|
||||||
|
|
||||||
langsmith@0.2.3(openai@4.69.0(zod@3.23.8)):
|
|
||||||
dependencies:
|
|
||||||
'@types/uuid': 10.0.0
|
|
||||||
commander: 10.0.1
|
|
||||||
p-queue: 6.6.2
|
|
||||||
p-retry: 4.6.2
|
|
||||||
semver: 7.6.0
|
|
||||||
uuid: 10.0.0
|
|
||||||
optionalDependencies:
|
|
||||||
openai: 4.69.0(zod@3.23.8)
|
|
||||||
|
|
||||||
langsmith@0.2.3(openai@4.69.0):
|
|
||||||
dependencies:
|
|
||||||
'@types/uuid': 10.0.0
|
|
||||||
commander: 10.0.1
|
|
||||||
p-queue: 6.6.2
|
|
||||||
p-retry: 4.6.2
|
|
||||||
semver: 7.6.0
|
|
||||||
uuid: 10.0.0
|
|
||||||
optionalDependencies:
|
|
||||||
openai: 4.69.0(zod@3.23.8)
|
|
||||||
|
|
||||||
lazy-ass@1.6.0: {}
|
lazy-ass@1.6.0: {}
|
||||||
|
|
||||||
ldapts@4.2.6:
|
ldapts@4.2.6:
|
||||||
|
@ -22002,6 +21965,8 @@ snapshots:
|
||||||
lodash: 4.17.21
|
lodash: 4.17.21
|
||||||
pretender: 3.4.7
|
pretender: 3.4.7
|
||||||
|
|
||||||
|
mitt@2.1.0: {}
|
||||||
|
|
||||||
mjml-accordion@4.15.3(encoding@0.1.13):
|
mjml-accordion@4.15.3(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/runtime': 7.24.7
|
'@babel/runtime': 7.24.7
|
||||||
|
@ -22823,22 +22788,6 @@ snapshots:
|
||||||
- encoding
|
- encoding
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
openai@4.69.0(zod@3.23.8):
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 18.16.16
|
|
||||||
'@types/node-fetch': 2.6.4
|
|
||||||
abort-controller: 3.0.0
|
|
||||||
agentkeepalive: 4.2.1
|
|
||||||
form-data-encoder: 1.7.2
|
|
||||||
formdata-node: 4.4.1
|
|
||||||
node-fetch: 2.7.0(encoding@0.1.13)
|
|
||||||
optionalDependencies:
|
|
||||||
zod: 3.23.8
|
|
||||||
transitivePeerDependencies:
|
|
||||||
- encoding
|
|
||||||
- supports-color
|
|
||||||
optional: true
|
|
||||||
|
|
||||||
openapi-sampler@1.5.1:
|
openapi-sampler@1.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/json-schema': 7.0.15
|
'@types/json-schema': 7.0.15
|
||||||
|
@ -23019,7 +22968,7 @@ snapshots:
|
||||||
|
|
||||||
pdf-parse@1.1.1:
|
pdf-parse@1.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
node-ensure: 0.0.0
|
node-ensure: 0.0.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
@ -23823,9 +23772,9 @@ snapshots:
|
||||||
|
|
||||||
ret@0.1.15: {}
|
ret@0.1.15: {}
|
||||||
|
|
||||||
retry-axios@2.6.0(axios@1.7.4(debug@4.3.7)):
|
retry-axios@2.6.0(axios@1.7.4):
|
||||||
dependencies:
|
dependencies:
|
||||||
axios: 1.7.4(debug@4.3.7)
|
axios: 1.7.4
|
||||||
|
|
||||||
retry-request@7.0.2(encoding@0.1.13):
|
retry-request@7.0.2(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -23850,7 +23799,7 @@ snapshots:
|
||||||
|
|
||||||
rhea@1.0.24:
|
rhea@1.0.24:
|
||||||
dependencies:
|
dependencies:
|
||||||
debug: 3.2.7(supports-color@5.5.0)
|
debug: 3.2.7(supports-color@8.1.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
|
@ -23988,7 +23937,7 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
chokidar: 4.0.1
|
chokidar: 4.0.1
|
||||||
immutable: 4.2.2
|
immutable: 4.2.2
|
||||||
source-map-js: 1.0.2
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
sax@1.2.4: {}
|
sax@1.2.4: {}
|
||||||
|
|
||||||
|
@ -24270,8 +24219,6 @@ snapshots:
|
||||||
smart-buffer: 4.2.0
|
smart-buffer: 4.2.0
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
source-map-js@1.0.2: {}
|
|
||||||
|
|
||||||
source-map-js@1.2.0: {}
|
source-map-js@1.2.0: {}
|
||||||
|
|
||||||
source-map-js@1.2.1: {}
|
source-map-js@1.2.1: {}
|
||||||
|
@ -25494,6 +25441,14 @@ snapshots:
|
||||||
markdown-it: 13.0.2
|
markdown-it: 13.0.2
|
||||||
vue: 3.5.11(typescript@5.7.2)
|
vue: 3.5.11(typescript@5.7.2)
|
||||||
|
|
||||||
|
vue-observe-visibility@2.0.0-alpha.1(vue@3.5.11(typescript@5.7.2)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.11(typescript@5.7.2)
|
||||||
|
|
||||||
|
vue-resize@2.0.0-alpha.1(vue@3.5.11(typescript@5.7.2)):
|
||||||
|
dependencies:
|
||||||
|
vue: 3.5.11(typescript@5.7.2)
|
||||||
|
|
||||||
vue-router@4.4.5(vue@3.5.11(typescript@5.7.2)):
|
vue-router@4.4.5(vue@3.5.11(typescript@5.7.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
|
@ -25506,6 +25461,13 @@ snapshots:
|
||||||
semver: 7.6.0
|
semver: 7.6.0
|
||||||
typescript: 5.7.2
|
typescript: 5.7.2
|
||||||
|
|
||||||
|
vue-virtual-scroller@2.0.0-beta.8(vue@3.5.11(typescript@5.7.2)):
|
||||||
|
dependencies:
|
||||||
|
mitt: 2.1.0
|
||||||
|
vue: 3.5.11(typescript@5.7.2)
|
||||||
|
vue-observe-visibility: 2.0.0-alpha.1(vue@3.5.11(typescript@5.7.2))
|
||||||
|
vue-resize: 2.0.0-alpha.1(vue@3.5.11(typescript@5.7.2))
|
||||||
|
|
||||||
vue3-touch-events@4.1.3: {}
|
vue3-touch-events@4.1.3: {}
|
||||||
|
|
||||||
vue@3.5.11(typescript@5.7.2):
|
vue@3.5.11(typescript@5.7.2):
|
||||||
|
|
Loading…
Reference in a new issue