mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
feat(editor-ui): JSON mapping (#4270)
* refactor(editor-ui): update 'vue-json-pretty' and adjust component to preserve same behaviour (#4152) * fix(editor-ui): export interface to solve 'TS4082: Default export of the module has or is using private name' error temporarily * refactor(editor-ui): update 'vue-json-pretty' and adjust component to preserve same behaviour * refactor(editor-ui): move json data view into its own component (#4158) * refactor(editor-ui): move json data view into its own component * fix(editor-ui): make JSON data component work again * fix(editor-ui): JSON data component type issues * fix(editor-ui): JSON data component prop 'inputData' * refactor(editor-ui): rename helper function * fix(editor-ui): add declaration to `vue-json-pretty` component * refactor(editor-ui): JSON mapping move more logic to new component * refactor(editor-ui): some cleanup in JSON mapping component * refactor(editor-ui): changing key mapping translation * refactor(editor-ui): add basic drag'n'drop functionality to JSON view * refactor(editor-ui): moving JSON view actions into separate components * fix(editor-ui): JSON view action copy default selected path * fix(editor-ui): refactor draggable to play nicer with other (3rd party) components * fix(editor-ui): improve draggable performance * fix(editor-ui): add disable user selection class to body * fix(editor-ui): reduce click handler cognitive load in JSON view copy actions * fix(editor-ui): JSON view mapped path * fix(editor-ui): remove unnecessary wrapper around RunDataTable.vue * fix(editor-ui): respect input node distance when json parameter path is copied * fix(editor-ui): JSON mapping property highlight * fix(editor-ui): block event only on mousemove for draggable to not select content * refactor(editor-ui): fixing prop types and organising imports * fix(editor-ui): JSON view use double quotes where appropriate * fix(editor-ui): fix new package additions after merge conflict * fix(editor-ui): fix package update after merge conflict * fix(editor-ui): JSON view prop names text break * fix(editor-ui): use kebab-case name for component * fix(editor-ui): calling convertPath on draggable node path * feat(editor-ui): add mapping discoverability tooltip to mappable inputs (#4227) * refactor(editor-ui): move json data view into its own component * fix(editor-ui): make JSON data component work again * fix(editor-ui): JSON data component type issues * fix(editor-ui): JSON data component prop 'inputData' * refactor(editor-ui): rename helper function * fix(editor-ui): add declaration to `vue-json-pretty` component * refactor(editor-ui): JSON mapping move more logic to new component * refactor(editor-ui): some cleanup in JSON mapping component * refactor(editor-ui): changing key mapping translation * refactor(editor-ui): add basic drag'n'drop functionality to JSON view * refactor(editor-ui): moving JSON view actions into separate components * fix(editor-ui): JSON view action copy default selected path * fix(editor-ui): refactor draggable to play nicer with other (3rd party) components * fix(editor-ui): improve draggable performance * fix(editor-ui): add disable user selection class to body * fix(editor-ui): reduce click handler cognitive load in JSON view copy actions * fix(editor-ui): JSON view mapped path * fix(editor-ui): remove unnecessary wrapper around RunDataTable.vue * fix(editor-ui): respect input node distance when json parameter path is copied * fix(editor-ui): JSON mapping property highlight * fix(editor-ui): block event only on mousemove for draggable to not select content * refactor(editor-ui): fixing prop types and organising imports * fix(editor-ui): JSON view use double quotes where appropriate * fix(editor-ui): fix new package additions after merge conflict * fix(editor-ui): fix package update after merge conflict * fix(editor-ui): JSON view prop names text break * fix(editor-ui): update helper after merge conflict * refactor(editor-ui): cleanup RunaDataTable tooltips * refactor(editor-ui): add temporary static tooltip to input with mapping * fix(editor-ui): input mapping tooltip proper input name * fix(editor-ui): show input mapping tooltip when conditions are met * fix(editor-ui): show different input mapping tooltip for different view types (table, json) * fix(editor-ui): drop lodash isEmpty * fix(editor-ui): using and keeping only getter function * fix(editor-ui): check `INodeExecutionData[]` array emptyness (still needs some improvement) * feat(editor-ui): add telemetry calls to data mapping (#4250) * fix(editor-ui): add types package for jsonpath * fix(editor-ui): JSON view drag'n'drop telemetry call * fix(editor-ui): add data mapping tooltip close telemetry to parameter input * fix(editor-ui): execute previous node tooltip linebreak * fix(editor-ui): input data mapping tooltip show-hide logic * fix(editor-ui): input data mapping tooltip position * fix(editor-ui): using a placeholder gif in mapping discoverability tooltip * refactor(design-system): adding optional configurable buttons to tooltip (#4260) * refactor(design-system): unbreaking wrapper around element ui tooltip * fix(design-system): update test snapshot * refactor(design-system): adding buttons to tooltip * fix(design-system): update test snapshot * fix(design-system): change tooltip props and some cleanup * fix(design-system): update test snapshot * chore: fix package lock file after merge * fix(editor-ui): modifications according to Max's review (#4273) * fix(editor-ui): modifications according to Max's review * fix(editor-ui): JSON prop names should not be written bold * fix(editor-ui): use proper animated gif in JSON data mapping discoverability tooltip
This commit is contained in:
@@ -131,37 +131,9 @@
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="[$style['data-container'], copyDropdownOpen ? $style['copy-dropdown-open'] : '']"
|
||||
:class="$style['data-container']"
|
||||
ref="dataContainer"
|
||||
>
|
||||
<div v-if="hasNodeRun && !hasRunError && displayMode === 'json'" v-show="!editMode.enabled" :class="$style['actions-group']">
|
||||
<el-dropdown
|
||||
trigger="click"
|
||||
@command="handleCopyClick"
|
||||
@visible-change="copyDropdownOpen = $event"
|
||||
>
|
||||
<span class="el-dropdown-link">
|
||||
<n8n-icon-button
|
||||
:title="$locale.baseText('runData.copyToClipboard')"
|
||||
icon="copy"
|
||||
type="tertiary"
|
||||
:circle="false"
|
||||
/>
|
||||
</span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{command: 'value'}">
|
||||
{{ $locale.baseText('runData.copyValue') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'itemPath'}" divided>
|
||||
{{ $locale.baseText('runData.copyItemPath') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="{command: 'parameterPath'}">
|
||||
{{ $locale.baseText('runData.copyParameterPath') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
|
||||
<div v-if="isExecuting" :class="$style.center">
|
||||
<div :class="$style.spinner"><n8n-spinner type="ring" /></div>
|
||||
<n8n-text>{{ executingMessage }}</n8n-text>
|
||||
@@ -239,25 +211,34 @@
|
||||
</n8n-text>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'table'" class="ph-no-capture" :class="$style.dataDisplay">
|
||||
<RunDataTable :node="node" :inputData="inputData" :mappingEnabled="mappingEnabled" :distanceFromActive="distanceFromActive" :showMappingHint="showMappingHint" :runIndex="runIndex" :totalRuns="maxRunIndex" @mounted="$emit('tableMounted', $event)" />
|
||||
</div>
|
||||
<run-data-table
|
||||
v-else-if="hasNodeRun && displayMode === 'table'"
|
||||
class="ph-no-capture"
|
||||
:node="node"
|
||||
:inputData="inputData"
|
||||
:mappingEnabled="mappingEnabled"
|
||||
:distanceFromActive="distanceFromActive"
|
||||
:showMappingHint="showMappingHint"
|
||||
:runIndex="runIndex"
|
||||
:totalRuns="maxRunIndex"
|
||||
@mounted="$emit('tableMounted', $event)"
|
||||
/>
|
||||
|
||||
<div v-else-if="hasNodeRun && displayMode === 'json'" class="ph-no-capture" :class="$style.jsonDisplay">
|
||||
<vue-json-pretty
|
||||
:data="jsonData"
|
||||
:deep="10"
|
||||
v-model="selectedOutput.path"
|
||||
:showLine="true"
|
||||
:showLength="true"
|
||||
selectableType="single"
|
||||
path=""
|
||||
:highlightSelectedNode="true"
|
||||
:selectOnClickNode="true"
|
||||
@click="dataItemClicked"
|
||||
class="json-data"
|
||||
/>
|
||||
</div>
|
||||
<run-data-json
|
||||
v-else-if="hasNodeRun && displayMode === 'json'"
|
||||
class="ph-no-capture"
|
||||
:paneType="paneType"
|
||||
:editMode="editMode"
|
||||
:currentOutputIndex="currentOutputIndex"
|
||||
:sessioId="sessionId"
|
||||
:node="node"
|
||||
:inputData="inputData"
|
||||
:mappingEnabled="mappingEnabled"
|
||||
:distanceFromActive="distanceFromActive"
|
||||
:showMappingHint="showMappingHint"
|
||||
:runIndex="runIndex"
|
||||
:totalRuns="maxRunIndex"
|
||||
/>
|
||||
|
||||
<div v-else-if="displayMode === 'binary' && binaryData.length === 0" :class="$style.center">
|
||||
<n8n-text align="center" tag="div">{{ $locale.baseText('runData.noBinaryDataFound') }}</n8n-text>
|
||||
@@ -338,8 +319,9 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
//@ts-ignore
|
||||
import VueJsonPretty from 'vue-json-pretty';
|
||||
import { PropType } from "vue";
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import { saveAs } from 'file-saver';
|
||||
import {
|
||||
IBinaryData,
|
||||
IBinaryKeyData,
|
||||
@@ -377,18 +359,12 @@ import { externalHooks } from "@/components/mixins/externalHooks";
|
||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||
import { pinData } from '@/components/mixins/pinData';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
import { CodeEditor } from "@/components/forms";
|
||||
import { dataPinningEventBus } from '../event-bus/data-pinning-event-bus';
|
||||
import { stringSizeInBytes } from './helpers';
|
||||
import { clearJsonKey, executionDataToJson, stringSizeInBytes } from './helpers';
|
||||
import RunDataTable from './RunDataTable.vue';
|
||||
import { isJsonKeyObject } from '@/utils';
|
||||
|
||||
// A path that does not exist so that nothing is selected by default
|
||||
const deselectedPlaceholder = '_!^&*';
|
||||
import RunDataJson from '@/components/RunDataJson.vue';
|
||||
import { isEmpty } from '@/utils';
|
||||
|
||||
export type EnterEditModeArgs = {
|
||||
origin: 'editIconButton' | 'insertTestDataLink',
|
||||
@@ -406,14 +382,15 @@ export default mixins(
|
||||
components: {
|
||||
BinaryDataDisplay,
|
||||
NodeErrorView,
|
||||
VueJsonPretty,
|
||||
WarningTooltip,
|
||||
CodeEditor,
|
||||
RunDataTable,
|
||||
RunDataJson,
|
||||
},
|
||||
props: {
|
||||
nodeUi: {
|
||||
}, // INodeUi | null
|
||||
type: Object as PropType<INodeUi>,
|
||||
},
|
||||
runIndex: {
|
||||
type: Number,
|
||||
},
|
||||
@@ -458,11 +435,6 @@ export default mixins(
|
||||
return {
|
||||
binaryDataPreviewActive: false,
|
||||
dataSize: 0,
|
||||
deselectedPlaceholder,
|
||||
selectedOutput: {
|
||||
value: '' as object | number | string,
|
||||
path: deselectedPlaceholder,
|
||||
},
|
||||
showData: false,
|
||||
outputIndex: 0,
|
||||
binaryDataDisplayVisible: false,
|
||||
@@ -473,7 +445,6 @@ export default mixins(
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
pageSizes: [10, 25, 50, 100],
|
||||
copyDropdownOpen: false,
|
||||
eventBus: dataPinningEventBus,
|
||||
|
||||
pinDataDiscoveryTooltipVisible: false,
|
||||
@@ -639,7 +610,7 @@ export default mixins(
|
||||
return inputData;
|
||||
},
|
||||
jsonData (): IDataObject[] {
|
||||
return this.convertToJson(this.inputData);
|
||||
return executionDataToJson(this.inputData);
|
||||
},
|
||||
binaryData (): IBinaryKeyData[] {
|
||||
if (!this.node) {
|
||||
@@ -728,8 +699,8 @@ export default mixins(
|
||||
},
|
||||
enterEditMode({ origin }: EnterEditModeArgs) {
|
||||
const inputData = this.pinData
|
||||
? this.clearJsonKey(this.pinData)
|
||||
: this.convertToJson(this.rawInputData);
|
||||
? clearJsonKey(this.pinData)
|
||||
: executionDataToJson(this.rawInputData);
|
||||
|
||||
const data = inputData.length > 0
|
||||
? inputData
|
||||
@@ -769,19 +740,12 @@ export default mixins(
|
||||
}
|
||||
|
||||
this.$store.commit('ui/setOutputPanelEditModeEnabled', false);
|
||||
this.$store.commit('pinData', { node: this.node, data: this.clearJsonKey(value) });
|
||||
this.$store.commit('pinData', { node: this.node, data: clearJsonKey(value) });
|
||||
|
||||
this.onDataPinningSuccess({ source: 'save-edit' });
|
||||
|
||||
this.onExitEditMode({ type: 'save' });
|
||||
},
|
||||
clearJsonKey(userInput: string | object) {
|
||||
const parsedUserInput = typeof userInput === 'string' ? JSON.parse(userInput) : userInput;
|
||||
|
||||
if (!Array.isArray(parsedUserInput)) return parsedUserInput;
|
||||
|
||||
return parsedUserInput.map(item => isJsonKeyObject(item) ? item.json : item);
|
||||
},
|
||||
onExitEditMode({ type }: { type: 'save' | 'cancel' }) {
|
||||
this.$telemetry.track('User closed ndv edit state', {
|
||||
node_type: this.activeNode.type,
|
||||
@@ -853,7 +817,7 @@ export default mixins(
|
||||
return;
|
||||
}
|
||||
|
||||
const data = this.convertToJson(this.rawInputData);
|
||||
const data = executionDataToJson(this.rawInputData);
|
||||
|
||||
if (!this.isValidPinDataSize(data)) {
|
||||
this.onDataPinningError({ errorType: 'data-too-large', source: 'pin-icon-click' });
|
||||
@@ -1018,24 +982,10 @@ export default mixins(
|
||||
this.binaryDataDisplayVisible = false;
|
||||
this.binaryDataDisplayData = null;
|
||||
},
|
||||
convertToJson (inputData: INodeExecutionData[]): IDataObject[] {
|
||||
const returnData: IDataObject[] = [];
|
||||
inputData.forEach((data) => {
|
||||
if (!data.hasOwnProperty('json')) {
|
||||
return;
|
||||
}
|
||||
returnData.push(data.json);
|
||||
});
|
||||
|
||||
return returnData;
|
||||
},
|
||||
clearExecutionData () {
|
||||
this.$store.commit('setWorkflowExecutionData', null);
|
||||
this.updateNodesExecutionIssues();
|
||||
},
|
||||
dataItemClicked (path: string, data: object | number | string) {
|
||||
this.selectedOutput.value = data;
|
||||
},
|
||||
isDownloadable (index: number, key: string): boolean {
|
||||
const binaryDataItem: IBinaryData = this.binaryData[index][key];
|
||||
return !!(binaryDataItem.mimeType && binaryDataItem.fileName);
|
||||
@@ -1077,123 +1027,6 @@ export default mixins(
|
||||
|
||||
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 isNotSelected = this.selectedOutput.path === deselectedPlaceholder;
|
||||
const selectedPath = isNotSelected ? '[""]' : this.selectedOutput.path;
|
||||
|
||||
let selectedValue = this.selectedOutput.value;
|
||||
if (isNotSelected) {
|
||||
if (this.hasPinData) {
|
||||
selectedValue = this.clearJsonKey(this.pinData as object);
|
||||
} else {
|
||||
selectedValue = this.convertToJson(this.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex));
|
||||
}
|
||||
}
|
||||
|
||||
const newPath = this.convertPath(selectedPath);
|
||||
|
||||
let value: string;
|
||||
if (commandData.command === 'value') {
|
||||
if (typeof selectedValue === 'object') {
|
||||
value = JSON.stringify(selectedValue, null, 2);
|
||||
} else {
|
||||
value = selectedValue.toString();
|
||||
}
|
||||
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('runData.copyValue.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} 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`;
|
||||
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('runData.copyItemPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
} else if (commandData.command === 'parameterPath') {
|
||||
path = newPath.split(']').slice(1).join(']');
|
||||
startPath = `$node["${this.node!.name}"].json`;
|
||||
|
||||
this.$showToast({
|
||||
title: this.$locale.baseText('runData.copyParameterPath.toast'),
|
||||
message: '',
|
||||
type: 'success',
|
||||
duration: 2000,
|
||||
});
|
||||
}
|
||||
if (!path.startsWith('[') && !path.startsWith('.') && path) {
|
||||
path += '.';
|
||||
}
|
||||
value = `{{ ${startPath + path} }}`;
|
||||
}
|
||||
|
||||
const copyType = {
|
||||
value: 'selection',
|
||||
itemPath: 'item_path',
|
||||
parameterPath: 'parameter_path',
|
||||
}[commandData.command];
|
||||
|
||||
this.$telemetry.track('User copied ndv data', {
|
||||
node_type: this.activeNode.type,
|
||||
session_id: this.sessionId,
|
||||
run_index: this.runIndex,
|
||||
view: this.displayMode,
|
||||
copy_type: copyType,
|
||||
workflow_id: this.$store.getters.workflowId,
|
||||
pane: 'output',
|
||||
in_execution_log: this.isReadOnly,
|
||||
});
|
||||
|
||||
this.copyToClipboard(value);
|
||||
},
|
||||
refreshDataSize () {
|
||||
// Hide by default the data from being displayed
|
||||
this.showData = false;
|
||||
@@ -1236,6 +1069,15 @@ export default mixins(
|
||||
node() {
|
||||
this.init();
|
||||
},
|
||||
inputData:{
|
||||
handler(data: INodeExecutionData[]) {
|
||||
if(this.paneType && data){
|
||||
this.$store.commit('ui/setNDVPanelDataIsEmpty', { panel: this.paneType, isEmpty: data.every(item => isEmpty(item.json)) });
|
||||
}
|
||||
},
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
jsonData (value: IDataObject[]) {
|
||||
this.refreshDataSize();
|
||||
|
||||
@@ -1312,36 +1154,23 @@ export default mixins(
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&:hover,
|
||||
&.copy-dropdown-open {
|
||||
&:hover{
|
||||
.actions-group {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dataDisplay {
|
||||
.errorDisplay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding-left: var(--spacing-s);
|
||||
padding: 0 var(--spacing-s) var(--spacing-3xl) var(--spacing-s);
|
||||
right: 0;
|
||||
overflow-y: auto;
|
||||
line-height: 1.5;
|
||||
word-break: normal;
|
||||
height: 100%;
|
||||
padding-bottom: var(--spacing-3xl);
|
||||
}
|
||||
|
||||
.errorDisplay {
|
||||
composes: dataDisplay;
|
||||
padding-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.jsonDisplay {
|
||||
composes: dataDisplay;
|
||||
background-color: var(--color-background-base);
|
||||
padding-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
@@ -1365,15 +1194,6 @@ export default mixins(
|
||||
}
|
||||
}
|
||||
|
||||
.actions-group {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 12px;
|
||||
right: var(--spacing-l);
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
@@ -1523,45 +1343,3 @@ export default mixins(
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
.vjs-tree {
|
||||
color: var(--color-json-default);
|
||||
}
|
||||
|
||||
.vjs-tree.is-highlight-selected {
|
||||
background-color: var(--color-json-highlight);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-value__null {
|
||||
color: var(--color-json-null);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-value__boolean {
|
||||
color: var(--color-json-boolean);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-value__number {
|
||||
color: var(--color-json-number);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-value__string {
|
||||
color: var(--color-json-string);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-key {
|
||||
color: var(--color-json-key);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-tree__brackets {
|
||||
color: var(--color-json-brackets);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-tree__brackets:hover {
|
||||
color: var(--color-json-brackets-hover);
|
||||
}
|
||||
|
||||
.vjs-tree .vjs-tree__content.has-line {
|
||||
border-left: 1px dotted var(--color-json-line);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user