mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
✨ Add functionality to easily copy data and path of output data (#1260)
* ✨ Add functionality to easily copy data and path of output data * ⚡ Fix issues with copied path * 👕 Fix lint issue * ;bug: Fix issue that some paths were wrong * ⚡ Final improvements
This commit is contained in:
parent
b5d4391ace
commit
bfb344a23c
|
@ -79,7 +79,7 @@
|
|||
"uuid": "^8.1.0",
|
||||
"vue": "^2.6.9",
|
||||
"vue-cli-plugin-webpack-bundle-analyzer": "^2.0.0",
|
||||
"vue-json-tree": "^0.4.1",
|
||||
"vue-json-pretty": "^1.7.1",
|
||||
"vue-prism-editor": "^0.3.0",
|
||||
"vue-router": "^3.0.6",
|
||||
"vue-template-compiler": "^2.5.17",
|
||||
|
|
|
@ -62,6 +62,21 @@
|
|||
<el-radio-button label="Binary" v-if="binaryData.length !== 0"></el-radio-button>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="select-button" v-if="displayMode === 'JSON' && state.path !== deselectedPlaceholder">
|
||||
<el-dropdown trigger="click" @command="handleCopyClick">
|
||||
<span class="el-dropdown-link">
|
||||
<el-button class="retry-button" circle type="text" size="small" title="Copy">
|
||||
<font-awesome-icon icon="copy" />
|
||||
</el-button>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'itemPath'}">Copy Item Path</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'parameterPath'}">Copy Parameter Path</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'value'}">Copy Value</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="data-display-content">
|
||||
<span v-if="node && workflowRunData !== null && workflowRunData.hasOwnProperty(node.name)">
|
||||
|
@ -104,10 +119,19 @@
|
|||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<json-tree
|
||||
<vue-json-pretty
|
||||
v-else-if="displayMode === 'JSON'"
|
||||
:data="jsonData"
|
||||
:level="10"
|
||||
:deep="10"
|
||||
v-model="state.path"
|
||||
:showLine="true"
|
||||
:showLength="true"
|
||||
selectableType="single"
|
||||
path=""
|
||||
:highlightSelectedNode="true"
|
||||
:selectOnClickNode="true"
|
||||
:custom-value-formatter="customLinkFormatter"
|
||||
@click="dataItemClicked"
|
||||
class="json-data"
|
||||
/>
|
||||
</div>
|
||||
|
@ -171,8 +195,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
// @ts-ignore
|
||||
import JsonTree from 'vue-json-tree';
|
||||
//@ts-ignore
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import {
|
||||
GenericValue,
|
||||
IBinaryData,
|
||||
|
@ -200,13 +224,18 @@ import {
|
|||
|
||||
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
|
||||
|
||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { workflowRun } from '@/components/mixins/workflowRun';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
// A path that does not exist so that nothing is selected by default
|
||||
const deselectedPlaceholder = '_!^&*';
|
||||
|
||||
export default mixins(
|
||||
copyPaste,
|
||||
genericHelpers,
|
||||
nodeHelpers,
|
||||
workflowRun,
|
||||
|
@ -215,13 +244,18 @@ export default mixins(
|
|||
name: 'RunData',
|
||||
components: {
|
||||
BinaryDataDisplay,
|
||||
JsonTree,
|
||||
VueJsonPretty,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
binaryDataPreviewActive: false,
|
||||
dataSize: 0,
|
||||
deselectedPlaceholder,
|
||||
displayMode: 'Table',
|
||||
state: {
|
||||
value: '' as object | number | string,
|
||||
path: deselectedPlaceholder,
|
||||
},
|
||||
runIndex: 0,
|
||||
showData: false,
|
||||
outputIndex: 0,
|
||||
|
@ -380,22 +414,17 @@ export default mixins(
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
getOutputName (outputIndex: number) {
|
||||
if (this.node === null) {
|
||||
return outputIndex + 1;
|
||||
}
|
||||
|
||||
const nodeType = this.$store.getters.nodeType(this.node.type);
|
||||
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
|
||||
return outputIndex + 1;
|
||||
}
|
||||
|
||||
return nodeType.outputNames[outputIndex];
|
||||
},
|
||||
closeBinaryDataDisplay () {
|
||||
this.binaryDataDisplayVisible = false;
|
||||
this.binaryDataDisplayData = null;
|
||||
},
|
||||
customLinkFormatter (data: object | number | string, key: string, parent: object, defaultFormatted: () => string) {
|
||||
if (typeof data === 'string' && data.startsWith('http://')) {
|
||||
return `<a style="color:red;" href="${data}" target="_blank">"${data}"</a>`;
|
||||
} else {
|
||||
return defaultFormatted;
|
||||
}
|
||||
},
|
||||
convertToJson (inputData: INodeExecutionData[]): IDataObject[] {
|
||||
const returnData: IDataObject[] = [];
|
||||
inputData.forEach((data) => {
|
||||
|
@ -465,7 +494,9 @@ export default mixins(
|
|||
this.$store.commit('setWorkflowExecutionData', null);
|
||||
this.updateNodesExecutionIssues();
|
||||
},
|
||||
// displayBinaryData (binaryData: IBinaryData) {
|
||||
dataItemClicked (path: string, data: object | number | string) {
|
||||
this.state.value = data;
|
||||
},
|
||||
displayBinaryData (index: number, key: string) {
|
||||
this.binaryDataDisplayVisible = true;
|
||||
|
||||
|
@ -477,6 +508,85 @@ export default mixins(
|
|||
key,
|
||||
};
|
||||
},
|
||||
getOutputName (outputIndex: number) {
|
||||
if (this.node === null) {
|
||||
return outputIndex + 1;
|
||||
}
|
||||
|
||||
const nodeType = this.$store.getters.nodeType(this.node.type);
|
||||
if (!nodeType.hasOwnProperty('outputNames') || nodeType.outputNames.length <= outputIndex) {
|
||||
return outputIndex + 1;
|
||||
}
|
||||
|
||||
return nodeType.outputNames[outputIndex];
|
||||
},
|
||||
convertPath (path: string): string {
|
||||
// TODO: That can for sure be done fancier but for now it works
|
||||
const placeholder = '*___~#^#~___*';
|
||||
let inBrackets = path.match(/\[(.*?)\]/g);
|
||||
|
||||
if (inBrackets === null) {
|
||||
inBrackets = [];
|
||||
} else {
|
||||
inBrackets = inBrackets.map(item => item.slice(1, -1)).map(item => {
|
||||
if (item.startsWith('"') && item.endsWith('"')) {
|
||||
return item.slice(1, -1);
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
const withoutBrackets = path.replace(/\[(.*?)\]/g, placeholder);
|
||||
const pathParts = withoutBrackets.split('.');
|
||||
const allParts = [] as string[];
|
||||
pathParts.forEach(part => {
|
||||
let index = part.indexOf(placeholder);
|
||||
while(index !== -1) {
|
||||
if (index === 0) {
|
||||
allParts.push(inBrackets!.shift() as string);
|
||||
part = part.substr(placeholder.length);
|
||||
} else {
|
||||
allParts.push(part.substr(0, index));
|
||||
part = part.substr(index);
|
||||
}
|
||||
index = part.indexOf(placeholder);
|
||||
}
|
||||
if (part !== '') {
|
||||
allParts.push(part);
|
||||
}
|
||||
});
|
||||
|
||||
return '["' + allParts.join('"]["') + '"]';
|
||||
},
|
||||
handleCopyClick (commandData: { command: string }) {
|
||||
const newPath = this.convertPath(this.state.path);
|
||||
|
||||
let value: string;
|
||||
if (commandData.command === 'value') {
|
||||
if (typeof this.state.value === 'object') {
|
||||
value = JSON.stringify(this.state.value, null, 2);
|
||||
} else {
|
||||
value = this.state.value.toString();
|
||||
}
|
||||
} else {
|
||||
let startPath = '';
|
||||
let path = '';
|
||||
if (commandData.command === 'itemPath') {
|
||||
const pathParts = newPath.split(']');
|
||||
const index = pathParts[0].slice(1);
|
||||
path = pathParts.slice(1).join(']');
|
||||
startPath = `$item(${index}).$node["${this.node!.name}"].json`;
|
||||
} else if (commandData.command === 'parameterPath') {
|
||||
path = newPath.split(']').slice(1).join(']');
|
||||
startPath = `$node["${this.node!.name}"].json`;
|
||||
}
|
||||
if (!path.startsWith('[') && !path.startsWith('.') && path) {
|
||||
path += '.';
|
||||
}
|
||||
value = `{{ ${startPath + path} }}`;
|
||||
}
|
||||
|
||||
this.copyToClipboard(value);
|
||||
},
|
||||
refreshDataSize () {
|
||||
// Hide by default the data from being displayed
|
||||
this.showData = false;
|
||||
|
@ -610,15 +720,8 @@ export default mixins(
|
|||
}
|
||||
|
||||
.json-data {
|
||||
.json-tree {
|
||||
&.vjs-tree {
|
||||
color: $--custom-input-font;
|
||||
|
||||
.json-tree-value-number {
|
||||
color: #b03030;
|
||||
}
|
||||
.json-tree-value-string {
|
||||
color: #8aab1a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -694,6 +797,16 @@ export default mixins(
|
|||
padding-top: 10px;
|
||||
padding-left: 10px;
|
||||
|
||||
.select-button {
|
||||
height: 30px;
|
||||
top: 50px;
|
||||
right: 30px;
|
||||
position: absolute;
|
||||
text-align: right;
|
||||
width: 200px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.title-text {
|
||||
display: inline-block;
|
||||
line-height: 30px;
|
||||
|
|
|
@ -5,6 +5,7 @@ import Vue from 'vue';
|
|||
import 'prismjs';
|
||||
import 'prismjs/themes/prism.css';
|
||||
import 'vue-prism-editor/dist/VuePrismEditor.css';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
import Vue2TouchEvents from 'vue2-touch-events';
|
||||
|
||||
import * as ElementUI from 'element-ui';
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
"noImplicitReturns": true,
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"skipLibCheck": true,
|
||||
"importHelpers": true,
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
|
|
Loading…
Reference in a new issue