From 541850f95f1c42fc16d9aeee3a3fef68a4b77082 Mon Sep 17 00:00:00 2001 From: OlegIvaniv Date: Thu, 16 Mar 2023 10:19:12 +0100 Subject: [PATCH] feat(editor): Add support for schema view in the NDV output (#5688) * feat(editor): Add support for schema view in the NDV output * Make intercepts waiting optional in waitForLoad method * Update RunDataSchema snapshots * Do not reset output panel view on execution, properly key run RunDataSchemaItem to make sure they are unique across panels * Update snapshot tests * Make adding of schema view button option more readable --- cypress/e2e/5-ndv.cy.ts | 38 ++++++++ cypress/e2e/7-workflow-actions.cy.ts | 2 + .../fixtures/Test_workflow_schema_test.json | 92 +++++++++++++++++++ cypress/pages/ndv.ts | 5 +- cypress/support/commands.ts | 8 +- cypress/support/index.ts | 2 +- packages/editor-ui/src/components/RunData.vue | 63 +++++++------ .../src/components/RunDataSchema.test.ts | 1 + .../src/components/RunDataSchema.vue | 2 + .../src/components/RunDataSchemaItem.vue | 6 +- .../__snapshots__/RunDataSchema.test.ts.snap | 28 ++++-- 11 files changed, 202 insertions(+), 45 deletions(-) create mode 100644 cypress/fixtures/Test_workflow_schema_test.json diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index a8f0d4cc9c..2c5c770a72 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -89,4 +89,42 @@ describe('NDV', () => { cy.get('[class*=hasIssues]').should('have.length', 1); }); }); + + describe('test output schema view', () => { + const schemaKeys = ['id', 'name', 'email', 'notes', 'country', 'created', 'objectValue', 'prop1', 'prop2']; + beforeEach(() => { + cy.createFixtureWorkflow('Test_workflow_schema_test.json', `NDV test schema view ${uuid()}`); + workflowPage.actions.zoomToFit(); + workflowPage.actions.openNode('Set'); + ndv.actions.execute(); + }); + it('should switch to output schema view and validate it', () => { + ndv.getters.outputDisplayMode().children().should('have.length', 3); + ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table'); + ndv.getters.outputDisplayMode().contains('Schema').click(); + ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); + + schemaKeys.forEach((key) => { + ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('exist'); + }); + }); + it('should preserve schema view after execution', () => { + ndv.getters.outputDisplayMode().contains('Schema').click(); + ndv.actions.execute(); + ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Schema'); + }) + it('should collapse and expand nested schema object', () => { + const expandedObjectProps = ['prop1', 'prop2'];; + const getObjectValueItem = () => ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').filter(':contains("objectValue")'); + ndv.getters.outputDisplayMode().contains('Schema').click(); + + expandedObjectProps.forEach((key) => { + ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('be.visible'); + }); + getObjectValueItem().find('label').click(); + expandedObjectProps.forEach((key) => { + ndv.getters.outputPanel().find('[data-test-id=run-data-schema-item]').contains(key).should('not.be.visible'); + }); + }) + }) }); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 07ab27afd0..cfce3cc9b0 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -94,6 +94,7 @@ describe('Workflow Actions', () => { cy.get('.el-message-box').should('be.visible'); cy.get('.el-message-box').find('input').type(IMPORT_WORKFLOW_URL); cy.get('body').type('{enter}'); + cy.waitForLoad(false) WorkflowPage.actions.zoomToFit(); WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.nodeConnections().should('have.length', 1); @@ -103,6 +104,7 @@ describe('Workflow Actions', () => { WorkflowPage.getters .workflowImportInput() .selectFile('cypress/fixtures/Test_workflow-actions_paste-data.json', { force: true }); + cy.waitForLoad(false) WorkflowPage.actions.zoomToFit(); WorkflowPage.getters.canvasNodes().should('have.length', 2); WorkflowPage.getters.nodeConnections().should('have.length', 1); diff --git a/cypress/fixtures/Test_workflow_schema_test.json b/cypress/fixtures/Test_workflow_schema_test.json new file mode 100644 index 0000000000..0db43a5ea4 --- /dev/null +++ b/cypress/fixtures/Test_workflow_schema_test.json @@ -0,0 +1,92 @@ +{ + "name": "My workflow 8", + "nodes": [ + { + "parameters": { + "operation": "getAllPeople", + "limit": 10 + }, + "id": "39cd80ce-5a8f-4339-b3d5-c4af969dd330", + "name": "Customer Datastore (n8n training)", + "type": "n8n-nodes-base.n8nTrainingCustomerDatastore", + "typeVersion": 1, + "position": [ + 940, + 680 + ] + }, + { + "parameters": { + "values": { + "number": [ + { + "name": "objectValue.prop1", + "value": 123 + } + ], + "string": [ + { + "name": "objectValue.prop2", + "value": "someText" + } + ] + }, + "options": { + "dotNotation": true + } + }, + "id": "6e4490f6-ba95-4400-beec-2caefdd4895a", + "name": "Set", + "type": "n8n-nodes-base.set", + "typeVersion": 1, + "position": [ + 1300, + 680 + ] + }, + { + "parameters": {}, + "id": "58512a93-dabf-4584-817f-27c608c1bdd5", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 720, + 680 + ] + } + ], + "pinData": {}, + "connections": { + "Customer Datastore (n8n training)": { + "main": [ + [ + { + "node": "Set", + "type": "main", + "index": 0 + } + ] + ] + }, + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Customer Datastore (n8n training)", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": {}, + "versionId": "4a4f292a-92be-427c-848a-9582527f5ed3", + "id": "8", + "meta": { + "instanceId": "032eceae7493054b723340499be69ecbf4cbe28a7ec6df676b759000750b968d" + }, + "tags": [] +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 500aec4c85..5a45f2df2d 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -13,10 +13,9 @@ export class NDV extends BasePage { outputPanel: () => cy.getByTestId('output-panel'), executingLoader: () => cy.getByTestId('ndv-executing'), inputDataContainer: () => this.getters.inputPanel().findChildByTestId('ndv-data-container'), - inputDisplayMode: () => this.getters.inputPanel().getByTestId('ndv-run-data-display-mode'), + inputDisplayMode: () => this.getters.inputPanel().findChildByTestId('ndv-run-data-display-mode').first(), outputDataContainer: () => this.getters.outputPanel().findChildByTestId('ndv-data-container'), - outputDisplayMode: () => this.getters.outputPanel().getByTestId('ndv-run-data-display-mode'), - digital: () => cy.getByTestId('ndv-run-data-display-mode'), + outputDisplayMode: () => this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(), pinDataButton: () => cy.getByTestId('ndv-pin-data'), editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'), pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor[role=code]'), diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 692abb2664..e096dc0aaa 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -39,6 +39,8 @@ Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => { workflowPage.getters .workflowImportInput() .selectFile(`cypress/fixtures/${fixtureKey}`, { force: true }); + + cy.waitForLoad(false); workflowPage.actions.setWorkflowName(workflowName); workflowPage.getters.saveButton().should('contain', 'Saved'); @@ -52,11 +54,13 @@ Cypress.Commands.add( }, ); -Cypress.Commands.add('waitForLoad', () => { +Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => { // These aliases are set-up before each test in cypress/support/e2e.ts // we can't set them up here because at this point it would be too late // and the requests would already have been made - cy.wait(['@loadSettings', '@loadLogin']) + if(waitForIntercepts) { + cy.wait(['@loadSettings', '@loadLogin']) + } cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist'); cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist'); }); diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 93927c98e8..b42db7e717 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -43,7 +43,7 @@ declare global { skipSetup(): void; resetAll(): void; enableFeature(feature: string): void; - waitForLoad(): void; + waitForLoad(waitForIntercepts?: boolean): void; grantBrowserPermissions(...permissions: string[]): void; readClipboard(): Chainable; paste(pastePayload: string): void; diff --git a/packages/editor-ui/src/components/RunData.vue b/packages/editor-ui/src/components/RunData.vue index cc33d9936c..cd72958357 100644 --- a/packages/editor-ui/src/components/RunData.vue +++ b/packages/editor-ui/src/components/RunData.vue @@ -4,7 +4,7 @@ v-if="canPinData && hasPinData && !editMode.enabled && !isProductionExecutionPreview" theme="secondary" icon="thumbtack" - :class="$style['pinned-data-callout']" + :class="$style.pinnedDataCallout" > {{ $locale.baseText('runData.pindata.thisDataIsPinned') }} @@ -78,7 +78,7 @@ :manual="isControlledPinDataTooltip" > -
+
-
+
{{ executingMessage }}
-
-
+
+
-
- +
+ {{ $locale.baseText('runData.editor.copyDataInfo') }} {{ $locale.baseText('generic.learnMore') }} @@ -329,6 +329,7 @@ :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :node="node" + :paneType="paneType" :runIndex="runIndex" :totalRuns="maxRunIndex" /> @@ -656,8 +657,11 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten defaults.push({ label: this.$locale.baseText('runData.binary'), value: 'binary' }); } + const schemaView = { label: this.$locale.baseText('runData.schema'), value: 'schema' }; if (this.isPaneTypeInput) { - defaults.unshift({ label: this.$locale.baseText('runData.schema'), value: 'schema' }); + defaults.unshift(schemaView); + } else { + defaults.push(schemaView); } if ( @@ -1305,10 +1309,12 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten this.activeNode.type === HTML_NODE_TYPE && this.activeNode.parameters.operation === 'generateHtmlTemplate'; - this.ndvStore.setPanelDisplayMode({ - pane: 'output', - mode: shouldDisplayHtml ? 'html' : 'table', - }); + if (shouldDisplayHtml) { + this.ndvStore.setPanelDisplayMode({ + pane: 'output', + mode: 'html', + }); + } }, }, watch: { @@ -1380,7 +1386,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten flex-direction: column; } -.pinned-data-callout { +.pinnedDataCallout { border-radius: inherit; border-bottom-right-radius: 0; border-top: 0; @@ -1403,7 +1409,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten } } -.data-container { +.dataContainer { position: relative; height: 100%; @@ -1421,7 +1427,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten padding: 0 var(--spacing-s) var(--spacing-3xl) var(--spacing-s); right: 0; overflow-y: auto; - line-height: 1.5; + line-height: var(--font-line-height-xloose); word-break: normal; height: 100%; } @@ -1500,10 +1506,10 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten .binaryHeader { color: $color-primary; - font-weight: 600; + font-weight: var(--font-weight-bold); font-size: 1.2em; - padding-bottom: 0.5em; - margin-bottom: 0.5em; + padding-bottom: var(--spacing-2xs); + margin-bottom: var(--spacing-2xs); border-bottom: 1px solid var(--color-text-light); } @@ -1529,12 +1535,11 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten justify-content: flex-end; flex-grow: 1; } - -.tooltip-container { +.tooltipContain { max-width: 240px; } -.pin-data-button { +.pinDataButton { svg { transition: transform 0.3s ease; } @@ -1552,7 +1557,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten margin-bottom: var(--spacing-s); } -.edit-mode { +.editMode { height: calc(100% - var(--spacing-s)); display: flex; flex-direction: column; @@ -1562,14 +1567,14 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten padding-right: var(--spacing-s); } -.edit-mode-body { +.editModeBody { flex: 1 1 auto; width: 100%; height: 100%; overflow: hidden; } -.edit-mode-footer { +.editModeFooter { display: flex; width: 100%; justify-content: space-between; @@ -1577,13 +1582,13 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten padding-top: var(--spacing-s); } -.edit-mode-footer-infotip { +.editModeFooterInfotip { display: flex; flex: 1; width: 100%; } -.edit-mode-actions { +.editModeActions { display: flex; justify-content: flex-end; align-items: center; diff --git a/packages/editor-ui/src/components/RunDataSchema.test.ts b/packages/editor-ui/src/components/RunDataSchema.test.ts index ff71b2aa00..0ae6b53458 100644 --- a/packages/editor-ui/src/components/RunDataSchema.test.ts +++ b/packages/editor-ui/src/components/RunDataSchema.test.ts @@ -24,6 +24,7 @@ describe('RunDataJsonSchema.vue', () => { distanceFromActive: 1, runIndex: 1, totalRuns: 2, + paneType: 'input', node: { parameters: { keepOnlySet: false, diff --git a/packages/editor-ui/src/components/RunDataSchema.vue b/packages/editor-ui/src/components/RunDataSchema.vue index 85de3f02c1..344dc44a21 100644 --- a/packages/editor-ui/src/components/RunDataSchema.vue +++ b/packages/editor-ui/src/components/RunDataSchema.vue @@ -18,6 +18,7 @@ type Props = { distanceFromActive: number; runIndex: number; totalRuns: number; + paneType: 'input' | 'output'; node: INodeUi | null; }; @@ -92,6 +93,7 @@ const onDragEnd = (el: HTMLElement) => { :schema="schema" :level="0" :parent="null" + :paneType="paneType" :subKey="`${schema.type}-0-0`" :mappingEnabled="mappingEnabled" :draggingPath="draggingPath" diff --git a/packages/editor-ui/src/components/RunDataSchemaItem.vue b/packages/editor-ui/src/components/RunDataSchemaItem.vue index e6457fe93a..23550593c0 100644 --- a/packages/editor-ui/src/components/RunDataSchemaItem.vue +++ b/packages/editor-ui/src/components/RunDataSchemaItem.vue @@ -9,6 +9,7 @@ type Props = { level: number; parent: Schema | null; subKey: string; + paneType: 'input' | 'output'; mappingEnabled: boolean; draggingPath: string; distanceFromActive: number; @@ -72,7 +73,7 @@ const getIconBySchemaType = (type: Schema['type']): string => {