diff --git a/packages/frontend/editor-ui/src/components/RunData.vue b/packages/frontend/editor-ui/src/components/RunData.vue index c0e77ea259..306d4de35e 100644 --- a/packages/frontend/editor-ui/src/components/RunData.vue +++ b/packages/frontend/editor-ui/src/components/RunData.vue @@ -1827,6 +1827,7 @@ defineExpose({ enterEditMode }); :mapping-enabled="mappingEnabled" :distance-from-active="distanceFromActive" :run-index="runIndex" + :output-index="currentOutputIndex" :total-runs="maxRunIndex" :search="search" /> diff --git a/packages/frontend/editor-ui/src/components/RunDataJson.vue b/packages/frontend/editor-ui/src/components/RunDataJson.vue index 66afeb8eb2..524807a87b 100644 --- a/packages/frontend/editor-ui/src/components/RunDataJson.vue +++ b/packages/frontend/editor-ui/src/components/RunDataJson.vue @@ -29,6 +29,7 @@ const props = withDefaults( inputData: INodeExecutionData[]; mappingEnabled?: boolean; distanceFromActive: number; + outputIndex: number | undefined; runIndex: number | undefined; totalRuns: number | undefined; search: string | undefined; @@ -45,7 +46,6 @@ const telemetry = useTelemetry(); const selectedJsonPath = ref(nonExistingJsonPath); const draggingPath = ref(null); -const displayMode = ref('json'); const jsonDataContainer = ref(null); const { height } = useElementSize(jsonDataContainer); @@ -119,12 +119,13 @@ const getListItemName = (path: string) => { { + return { + useRouter: () => ({}), + useRoute: () => reactive({ meta: {} }), + RouterLink: vi.fn(), + }; +}); + +const copy = vi.fn(); +vi.mock('@/composables/useClipboard', () => ({ + useClipboard: () => ({ + copy, + }), +})); + +const i18n = useI18n(); + +async function createPiniaWithActiveNode() { + const node = mockNodes[0]; + const workflow = mock({ + id: '1', + name: 'Test Workflow', + versionId: '1', + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + active: true, + connections: {}, + nodes: [node], + }); + + const pinia = createPinia(); + setActivePinia(pinia); + + const workflowsStore = useWorkflowsStore(); + const nodeTypesStore = useNodeTypesStore(); + const ndvStore = useNDVStore(); + + nodeTypesStore.setNodeTypes(defaultNodeDescriptions); + workflowsStore.workflow = workflow; + workflowsStore.nodeMetadata[node.name] = { pristine: true }; + workflowsStore.workflowExecutionData = { + id: '1', + finished: true, + mode: 'trigger', + status: 'success', + createdAt: new Date(), + startedAt: new Date(), + workflowData: workflow, + data: { + resultData: { + runData: { + [node.name]: [ + { + startTime: new Date().getTime(), + executionTime: new Date().getTime(), + data: { + main: [ + [ + { + json: { + id: 1, + name: 'First run 1', + }, + }, + { + json: { + id: 2, + name: 'First run 2', + }, + }, + ], + ], + }, + source: [null], + }, + { + startTime: new Date().getTime(), + executionTime: new Date().getTime(), + data: { + main: [ + [ + { + json: { + id: 3, + name: 'Second run 1', + }, + }, + ], + ], + }, + source: [null], + }, + ], + }, + }, + }, + }; + + ndvStore.activeNodeName = node.name; + + return { + pinia, + activeNode: ndvStore.activeNode, + }; +} + +describe('RunDataJsonActions', () => { + let server: ReturnType; + + beforeEach(cleanup); + + beforeAll(() => { + document.body.innerHTML = '
'; + server = setupServer(); + }); + + afterEach(() => { + copy.mockReset(); + vi.clearAllMocks(); + }); + + afterAll(() => { + server.shutdown(); + }); + + it('should copy unselected JSON output from latest run on click', async () => { + const { pinia, activeNode } = await createPiniaWithActiveNode(); + const renderComponent = createComponentRenderer(RunDataJsonActions, { + props: { + node: activeNode, + paneType: 'output', + pushRef: 'ref', + displayMode: 'json', + distanceFromActive: 0, + selectedJsonPath: nonExistingJsonPath, + jsonData: [ + { + id: 3, + name: 'Second run 1', + }, + ], + outputIndex: 0, + runIndex: 1, + }, + global: { + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + }, + }); + + const { getByTestId } = renderComponent({ + pinia, + }); + + await waitFor(() => expect(getByTestId('ndv-json-actions')).toBeInTheDocument()); + + const button = within(getByTestId('ndv-json-actions')).getByRole('button'); + + await fireEvent.click(button); + + expect(copy).toHaveBeenCalledWith( + JSON.stringify( + [ + { + id: 3, + name: 'Second run 1', + }, + ], + null, + 2, + ), + ); + }); + + it('should copy unselected JSON output from selected previous run on click', async () => { + const { pinia, activeNode } = await createPiniaWithActiveNode(); + const renderComponent = createComponentRenderer(RunDataJsonActions, { + props: { + node: activeNode, + paneType: 'output', + pushRef: 'ref', + displayMode: 'json', + distanceFromActive: 0, + selectedJsonPath: nonExistingJsonPath, + jsonData: [ + { + id: 3, + name: 'Second run 1', + }, + ], + outputIndex: 0, + runIndex: 0, + }, + global: { + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + }, + }); + + const { getByTestId } = renderComponent({ + pinia, + }); + + await waitFor(() => expect(getByTestId('ndv-json-actions')).toBeInTheDocument()); + + const button = within(getByTestId('ndv-json-actions')).getByRole('button'); + + await fireEvent.click(button); + + expect(copy).toHaveBeenCalledWith( + JSON.stringify( + [ + { + id: 1, + name: 'First run 1', + }, + { + id: 2, + name: 'First run 2', + }, + ], + null, + 2, + ), + ); + }); + + it("should copy selected JSON value on 'Copy Selection' click", async () => { + const { pinia, activeNode } = await createPiniaWithActiveNode(); + const renderComponent = createComponentRenderer(RunDataJsonActions, { + props: { + node: activeNode, + paneType: 'output', + pushRef: 'ref', + displayMode: 'json', + distanceFromActive: 0, + selectedJsonPath: '[0].name', + jsonData: [ + { + id: 3, + name: 'Second run 1', + }, + ], + outputIndex: 0, + runIndex: 1, + }, + global: { + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + }, + }); + + renderComponent({ + pinia, + }); + + await waitFor(() => + expect(screen.getByText(i18n.baseText('runData.copyValue'))).toBeInTheDocument(), + ); + + const option = screen.getByText(i18n.baseText('runData.copyValue')); + + await fireEvent.click(option); + + expect(copy).toHaveBeenCalledWith('Second run 1'); + }); + + it("should copy selected JSON value's item path on 'Copy Item Path' click", async () => { + const { pinia, activeNode } = await createPiniaWithActiveNode(); + const renderComponent = createComponentRenderer(RunDataJsonActions, { + props: { + node: activeNode, + paneType: 'output', + pushRef: 'ref', + displayMode: 'json', + distanceFromActive: 0, + selectedJsonPath: '[0].name', + jsonData: [ + { + id: 3, + name: 'Second run 1', + }, + ], + outputIndex: 0, + runIndex: 1, + }, + global: { + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + }, + }); + + renderComponent({ + pinia, + }); + + await waitFor(() => + expect(screen.getByText(i18n.baseText('runData.copyItemPath'))).toBeInTheDocument(), + ); + + const option = screen.getByText(i18n.baseText('runData.copyItemPath')); + + await fireEvent.click(option); + + expect(copy).toHaveBeenCalledWith('{{ $item("0").$node["Manual Trigger"].json["name"] }}'); + }); + + it("should copy selected JSON value's parameter path on 'Copy Parameter Path' click", async () => { + const { pinia, activeNode } = await createPiniaWithActiveNode(); + const renderComponent = createComponentRenderer(RunDataJsonActions, { + props: { + node: activeNode, + paneType: 'output', + pushRef: 'ref', + displayMode: 'json', + distanceFromActive: 0, + selectedJsonPath: '[0].name', + jsonData: [ + { + id: 3, + name: 'Second run 1', + }, + ], + outputIndex: 0, + runIndex: 1, + }, + global: { + mocks: { + $route: { + name: VIEWS.WORKFLOW, + }, + }, + }, + }); + + renderComponent({ + pinia, + }); + + await waitFor(() => + expect(screen.getByText(i18n.baseText('runData.copyParameterPath'))).toBeInTheDocument(), + ); + + const option = screen.getByText(i18n.baseText('runData.copyParameterPath')); + + await fireEvent.click(option); + + expect(copy).toHaveBeenCalledWith('{{ $node["Manual Trigger"].json["name"] }}'); + }); +}); diff --git a/packages/frontend/editor-ui/src/components/RunDataJsonActions.vue b/packages/frontend/editor-ui/src/components/RunDataJsonActions.vue index e5b11b6ca2..3c84c3babd 100644 --- a/packages/frontend/editor-ui/src/components/RunDataJsonActions.vue +++ b/packages/frontend/editor-ui/src/components/RunDataJsonActions.vue @@ -26,12 +26,11 @@ const props = withDefaults( node: INodeUi; paneType: string; pushRef: string; - displayMode: string; distanceFromActive: number; selectedJsonPath: string; jsonData: IDataObject[]; - currentOutputIndex?: number; - runIndex?: number; + outputIndex: number | undefined; + runIndex: number | undefined; }>(), { selectedJsonPath: nonExistingJsonPath, @@ -71,7 +70,7 @@ function getJsonValue(): string { selectedValue = clearJsonKey(pinnedData.data.value as object); } else { selectedValue = executionDataToJson( - nodeHelpers.getNodeInputData(props.node, props.runIndex, props.currentOutputIndex), + nodeHelpers.getNodeInputData(props.node, props.runIndex, props.outputIndex), ); } } @@ -176,7 +175,7 @@ function handleCopyClick(commandData: { command: string }) {