mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-15 17:14:05 -08:00
4528f34462
* feat(editor): Generate custom schema from data (#4562) * feat(core): adding a type package to n8n * feat(editor): adding custom schema generator * fix: add new types package to lock file * fix: remove n8n_io/types package * fix: adding path to generated schema * fix: handling nested lists in schema generation * fix: add date support to schema generation * fix: define dates in ISO format * fix: using test instead of it in repeated tests * fix(editor): JSON schema treat nested lists as object to allow mapping each level * fix(editor): rename JSON schema type * fix(editor): make JSON schema path required * fix(editor): using JSON schema bracket notation for object props to handle exceptional keys * fix(editor): reorder JSON schema generator function args * feat(editor): Add date recognizer util function (#4620) * ✨ Implemented date recogniser fuction * ✅ Added unit tests for date recogniser * ✔️ Fixing linting errors * 👌 Updating test cases * feat(editor): Implement JSON Schema view UI functionalities (#4601) * feat(core): adding a type package to n8n * feat(editor): adding custom schema generator * fix: add new types package to lock file * fix: remove n8n_io/types package * fix: adding path to generated schema * fix: handling nested lists in schema generation * fix: add date support to schema generation * fix: define dates in ISO format * fix: using test instead of it in repeated tests * fix(editor): JSON schema treat nested lists as object to allow mapping each level * fix(editor): rename JSON schema type * fix(editor): make JSON schema path required * fix(editor): using JSON schema bracket notation for object props to handle exceptional keys * fix(editor): reorder JSON schema generator function args * fix(editor): WIP json schema view * fix(editor): formatting fix * fix(editor): WIP json schema viewer * fix(editor): fix schema generator and add deep merge * fix(editor): WIP update json schema view components * fix(editor): extend valid date checking * fix(editor): WIP improving JSON schema view * chore(editor): code formatting * feat(editor): WIP Json schema view mapping + animations * feat(editor): WIP update mergeDeep * feat(editor): adding first item of json data to the end once more to get sample data from the first item * feat(editor): adding first item of json data to the end once more to get sample data from the first item * fix(editor): improving draggable design * fix(editor): move util functions to their correct place after merge conflict * fix(editor): move some type guards * fix(editor): move some type guards * fix(editor): change import path in unit test * fix(editor): import missing interface * fix(editor): remove unused functions and parts from json schema generation * feat(editor): Add telemetry calls to JSON schema mapping (#4695) * feat(editor): WIP JSON schema telemetry call * feat(editor): make telemetry usable outside of Vue component context * chore(editor): remove unused variable * Merge branch 'feature/json-schema-view' of github.com:n8n-io/n8n into n8n-5410-add-telemetry-calls # Conflicts: # packages/editor-ui/src/components/RunDataJsonSchema.vue * fix(editor): VUE typing for telemetry * fix(editor): enable PostHog feature flag * fix(editor): Schema design review (#4740) * refactor(editor): rename JsonSchema to Schema * fix(editor): schema component name * fix(editor): schema pill style * fix(editor): schema type date as string * fix(editor): schema styles (support long text + firefox) * fix(editor): schema truncate text if it's too long * fix(editor): schema types * fix(editor): droppable styles * fix(editor): schema component props * fix(editor): fix draggable pill styles * fix(editor): schema view styles * fix(editor): schema mapping tooltip * fix(editor): schema mapping styles * fix(editor): mapping styles * fix(editor): empty schema case * fix(editor): delay mapping tooltip * test(editor): add schema view snapshot test * fix(editor): schema empty string * fix(editor): schema string without space * fix(editor): update schema test snapshot * fix(editor): applying review comments * fix(editor): make n8nExternalHooks optional * fix(editor): remove TODO comment Co-authored-by: Milorad FIlipović <milorad@n8n.io>
451 lines
13 KiB
Vue
451 lines
13 KiB
Vue
<template>
|
|
<div>
|
|
<div :class="$style.inputPanel" v-if="!hideInputAndOutput" :style="inputPanelStyles">
|
|
<slot name="input"></slot>
|
|
</div>
|
|
<div :class="$style.outputPanel" v-if="!hideInputAndOutput" :style="outputPanelStyles">
|
|
<slot name="output"></slot>
|
|
</div>
|
|
<div :class="$style.mainPanel" :style="mainPanelStyles">
|
|
<n8n-resize-wrapper
|
|
:isResizingEnabled="currentNodePaneType !== 'unknown'"
|
|
:width="relativeWidthToPx(mainPanelDimensions.relativeWidth)"
|
|
:minWidth="MIN_PANEL_WIDTH"
|
|
:gridSize="20"
|
|
@resize="onResizeDebounced"
|
|
@resizestart="onResizeStart"
|
|
@resizeend="onResizeEnd"
|
|
:supportedDirections="supportedResizeDirections"
|
|
>
|
|
<div :class="$style.dragButtonContainer">
|
|
<PanelDragButton
|
|
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
|
|
:canMoveLeft="canMoveLeft"
|
|
:canMoveRight="canMoveRight"
|
|
v-if="!hideInputAndOutput && isDraggable"
|
|
@dragstart="onDragStart"
|
|
@drag="onDrag"
|
|
@dragend="onDragEnd"
|
|
/>
|
|
</div>
|
|
<div :class="{ [$style.mainPanelInner]: true, [$style.dragging]: isDragging }">
|
|
<slot name="main" />
|
|
</div>
|
|
</n8n-resize-wrapper>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import Vue, { PropType } from 'vue';
|
|
import { get } from 'lodash';
|
|
|
|
import { INodeTypeDescription } from 'n8n-workflow';
|
|
import PanelDragButton from './PanelDragButton.vue';
|
|
|
|
import {
|
|
LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH,
|
|
MAIN_NODE_PANEL_WIDTH,
|
|
} from '@/constants';
|
|
import mixins from 'vue-typed-mixins';
|
|
import { debounceHelper } from '@/mixins/debounce';
|
|
import { mapStores } from 'pinia';
|
|
import { useNDVStore } from '@/stores/ndv';
|
|
import { NodePanelType } from '@/Interface';
|
|
|
|
|
|
const SIDE_MARGIN = 24;
|
|
const SIDE_PANELS_MARGIN = 80;
|
|
const MIN_PANEL_WIDTH = 280;
|
|
const PANEL_WIDTH = 320;
|
|
const PANEL_WIDTH_LARGE = 420;
|
|
|
|
const initialMainPanelWidth:{ [key: string]: number } = {
|
|
regular: MAIN_NODE_PANEL_WIDTH,
|
|
dragless: MAIN_NODE_PANEL_WIDTH,
|
|
unknown: MAIN_NODE_PANEL_WIDTH,
|
|
inputless: MAIN_NODE_PANEL_WIDTH,
|
|
wide: MAIN_NODE_PANEL_WIDTH * 2,
|
|
};
|
|
|
|
export default mixins(debounceHelper).extend({
|
|
name: 'NDVDraggablePanels',
|
|
components: {
|
|
PanelDragButton,
|
|
},
|
|
props: {
|
|
isDraggable: {
|
|
type: Boolean,
|
|
},
|
|
hideInputAndOutput: {
|
|
type: Boolean,
|
|
},
|
|
position: {
|
|
type: Number,
|
|
},
|
|
nodeType: {
|
|
type: Object as PropType<INodeTypeDescription>,
|
|
default: () => ({}),
|
|
},
|
|
},
|
|
data(): { windowWidth: number, isDragging: boolean, MIN_PANEL_WIDTH: number, initialized: boolean} {
|
|
return {
|
|
windowWidth: 1,
|
|
isDragging: false,
|
|
MIN_PANEL_WIDTH,
|
|
initialized: false,
|
|
};
|
|
},
|
|
mounted() {
|
|
this.setTotalWidth();
|
|
|
|
/*
|
|
Only set(or restore) initial position if `mainPanelDimensions`
|
|
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
|
|
*/
|
|
if(this.mainPanelDimensions.relativeLeft === 1 && this.mainPanelDimensions.relativeRight === 1) {
|
|
this.setMainPanelWidth();
|
|
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
|
|
this.restorePositionData();
|
|
}
|
|
|
|
window.addEventListener('resize', this.setTotalWidth);
|
|
this.$emit('init', { position: this.mainPanelDimensions.relativeLeft });
|
|
setTimeout(() => {
|
|
this.initialized = true;
|
|
}, 0);
|
|
},
|
|
destroyed() {
|
|
window.removeEventListener('resize', this.setTotalWidth);
|
|
},
|
|
computed: {
|
|
...mapStores(
|
|
useNDVStore,
|
|
),
|
|
mainPanelDimensions(): {
|
|
relativeWidth: number,
|
|
relativeLeft: number,
|
|
relativeRight: number
|
|
} {
|
|
return this.ndvStore.getMainPanelDimensions(this.currentNodePaneType);
|
|
},
|
|
supportedResizeDirections(): string[] {
|
|
const supportedDirections = ['right'];
|
|
|
|
if(this.isDraggable) supportedDirections.push('left');
|
|
return supportedDirections;
|
|
},
|
|
currentNodePaneType(): string {
|
|
if(!this.hasInputSlot) return 'inputless';
|
|
if(!this.isDraggable) return 'dragless';
|
|
if(this.nodeType === null) return 'unknown';
|
|
return get(this, 'nodeType.parameterPane') || 'regular';
|
|
},
|
|
hasInputSlot(): boolean {
|
|
return this.$slots.input !== undefined;
|
|
},
|
|
inputPanelMargin(): number {
|
|
return this.pxToRelativeWidth(SIDE_PANELS_MARGIN);
|
|
},
|
|
minWindowWidth(): number {
|
|
return 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
|
|
},
|
|
minimumLeftPosition(): number {
|
|
if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
|
|
|
|
if(!this.hasInputSlot) return this.pxToRelativeWidth(SIDE_MARGIN);
|
|
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
|
|
},
|
|
maximumRightPosition(): number {
|
|
if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
|
|
|
|
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
|
|
},
|
|
canMoveLeft(): boolean {
|
|
return this.mainPanelDimensions.relativeLeft > this.minimumLeftPosition;
|
|
},
|
|
canMoveRight(): boolean {
|
|
return this.mainPanelDimensions.relativeRight > this.maximumRightPosition;
|
|
},
|
|
mainPanelStyles(): { left: string, right: string } {
|
|
return {
|
|
'left': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeLeft)}px`,
|
|
'right': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeRight)}px`,
|
|
};
|
|
},
|
|
inputPanelStyles():{ right: string } {
|
|
return {
|
|
right: `${this.relativeWidthToPx(this.calculatedPositions.inputPanelRelativeRight)}px`,
|
|
};
|
|
},
|
|
outputPanelStyles(): { left: string, transform: string} {
|
|
return {
|
|
left: `${this.relativeWidthToPx(this.calculatedPositions.outputPanelRelativeLeft)}px`,
|
|
transform: `translateX(-${this.relativeWidthToPx(this.outputPanelRelativeTranslate)}px)`,
|
|
};
|
|
},
|
|
calculatedPositions():{ inputPanelRelativeRight: number, outputPanelRelativeLeft: number } {
|
|
const hasInput = this.$slots.input !== undefined;
|
|
const outputPanelRelativeLeft = this.mainPanelDimensions.relativeLeft + this.mainPanelDimensions.relativeWidth;
|
|
|
|
const inputPanelRelativeRight = hasInput
|
|
? 1 - outputPanelRelativeLeft + this.mainPanelDimensions.relativeWidth
|
|
: (1 - this.pxToRelativeWidth(SIDE_MARGIN));
|
|
|
|
return {
|
|
inputPanelRelativeRight,
|
|
outputPanelRelativeLeft,
|
|
};
|
|
},
|
|
outputPanelRelativeTranslate():number {
|
|
const panelMinLeft = 1 - this.pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
|
|
const currentRelativeLeftDelta = this.calculatedPositions.outputPanelRelativeLeft - panelMinLeft;
|
|
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
|
|
},
|
|
hasDoubleWidth(): boolean {
|
|
return get(this, 'nodeType.parameterPane') === 'wide';
|
|
},
|
|
fixedPanelWidth(): number {
|
|
const multiplier = this.hasDoubleWidth ? 2 : 1;
|
|
|
|
if (this.windowWidth > 1700) {
|
|
return PANEL_WIDTH_LARGE * multiplier;
|
|
}
|
|
|
|
return PANEL_WIDTH * multiplier;
|
|
},
|
|
isBelowMinWidthMainPanel(): boolean {
|
|
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
|
|
return this.mainPanelDimensions.relativeWidth < minRelativeWidth;
|
|
},
|
|
},
|
|
watch: {
|
|
windowWidth(windowWidth) {
|
|
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
|
|
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
|
|
if(this.isBelowMinWidthMainPanel) {
|
|
this.setMainPanelWidth(minRelativeWidth);
|
|
}
|
|
|
|
const isBelowMinLeft = this.minimumLeftPosition > this.mainPanelDimensions.relativeLeft;
|
|
const isMaxRight = this.maximumRightPosition > this.mainPanelDimensions.relativeRight;
|
|
|
|
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
|
|
if((windowWidth > this.minWindowWidth) && isBelowMinLeft && isMaxRight) {
|
|
this.setMainPanelWidth(minRelativeWidth);
|
|
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
|
|
}
|
|
|
|
this.setPositions(this.mainPanelDimensions.relativeLeft);
|
|
},
|
|
},
|
|
methods: {
|
|
getInitialLeftPosition(width: number) {
|
|
if(this.currentNodePaneType === 'dragless') return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
|
|
|
|
return this.hasInputSlot
|
|
? 0.5 - (width / 2)
|
|
: this.minimumLeftPosition;
|
|
},
|
|
setMainPanelWidth(relativeWidth?: number) {
|
|
const mainPanelRelativeWidth = relativeWidth || this.pxToRelativeWidth(initialMainPanelWidth[this.currentNodePaneType]);
|
|
|
|
this.ndvStore.setMainPanelDimensions({
|
|
panelType: this.currentNodePaneType,
|
|
dimensions: {
|
|
relativeWidth: mainPanelRelativeWidth,
|
|
},
|
|
});
|
|
},
|
|
setPositions(relativeLeft: number) {
|
|
const mainPanelRelativeLeft = relativeLeft || 1 - this.calculatedPositions.inputPanelRelativeRight;
|
|
const mainPanelRelativeRight = 1 - mainPanelRelativeLeft - this.mainPanelDimensions.relativeWidth;
|
|
|
|
const isMaxRight = this.maximumRightPosition > mainPanelRelativeRight;
|
|
const isMinLeft = this.minimumLeftPosition > mainPanelRelativeLeft;
|
|
const isInputless = this.currentNodePaneType === 'inputless';
|
|
|
|
if(isMinLeft) {
|
|
this.ndvStore.setMainPanelDimensions({
|
|
panelType: this.currentNodePaneType,
|
|
dimensions: {
|
|
relativeLeft: this.minimumLeftPosition,
|
|
relativeRight: 1 - this.mainPanelDimensions.relativeWidth - this.minimumLeftPosition,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
if(isMaxRight) {
|
|
this.ndvStore.setMainPanelDimensions({
|
|
panelType: this.currentNodePaneType,
|
|
dimensions: {
|
|
relativeLeft: 1 - this.mainPanelDimensions.relativeWidth - this.maximumRightPosition,
|
|
relativeRight: this.maximumRightPosition as number,
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
this.ndvStore.setMainPanelDimensions({
|
|
panelType: this.currentNodePaneType,
|
|
dimensions: {
|
|
relativeLeft: isInputless ? this.minimumLeftPosition : mainPanelRelativeLeft,
|
|
relativeRight: mainPanelRelativeRight,
|
|
},
|
|
});
|
|
},
|
|
pxToRelativeWidth(px: number) {
|
|
return px / this.windowWidth;
|
|
},
|
|
relativeWidthToPx(relativeWidth: number) {
|
|
return relativeWidth * this.windowWidth;
|
|
},
|
|
onResizeStart() {
|
|
this.setTotalWidth();
|
|
},
|
|
onResizeEnd() {
|
|
this.storePositionData();
|
|
},
|
|
onResizeDebounced(data: { direction: string, x: number, width: number}) {
|
|
if (this.initialized) {
|
|
this.callDebounced('onResize', { debounceTime: 10, trailing: true }, data);
|
|
}
|
|
},
|
|
onResize({ direction, x, width }: { direction: string, x: number, width: number}) {
|
|
const relativeDistance = this.pxToRelativeWidth(x);
|
|
const relativeWidth = this.pxToRelativeWidth(width);
|
|
|
|
if(direction === "left" && relativeDistance <= this.minimumLeftPosition) return;
|
|
if(direction === "right" && (1 - relativeDistance) <= this.maximumRightPosition) return;
|
|
if(width <= MIN_PANEL_WIDTH) return;
|
|
|
|
this.setMainPanelWidth(relativeWidth);
|
|
this.setPositions(direction === 'left'
|
|
? relativeDistance
|
|
: this.mainPanelDimensions.relativeLeft,
|
|
);
|
|
},
|
|
restorePositionData() {
|
|
const storedPanelWidthData = window.localStorage.getItem(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`);
|
|
|
|
if(storedPanelWidthData) {
|
|
const parsedWidth = parseFloat(storedPanelWidthData);
|
|
this.setMainPanelWidth(parsedWidth);
|
|
const initialPosition = this.getInitialLeftPosition(parsedWidth);
|
|
|
|
this.setPositions(initialPosition);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
storePositionData() {
|
|
window.localStorage.setItem(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`, this.mainPanelDimensions.relativeWidth.toString());
|
|
},
|
|
onDragStart() {
|
|
this.isDragging = true;
|
|
this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
|
|
},
|
|
onDrag(e: {x: number, y: number}) {
|
|
const relativeLeft = this.pxToRelativeWidth(e.x) - (this.mainPanelDimensions.relativeWidth / 2);
|
|
|
|
this.setPositions(relativeLeft);
|
|
},
|
|
onDragEnd() {
|
|
setTimeout(() => {
|
|
this.isDragging = false;
|
|
this.$emit('dragend', {
|
|
windowWidth: this.windowWidth,
|
|
position: this.mainPanelDimensions.relativeLeft,
|
|
});
|
|
}, 0);
|
|
this.storePositionData();
|
|
},
|
|
setTotalWidth() {
|
|
this.windowWidth = window.innerWidth;
|
|
},
|
|
close() {
|
|
this.$emit('close');
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
.dataPanel {
|
|
position: absolute;
|
|
height: calc(100% - 2 * var(--spacing-l));
|
|
position: absolute;
|
|
top: var(--spacing-l);
|
|
z-index: 0;
|
|
min-width: 280px;
|
|
}
|
|
|
|
.inputPanel {
|
|
composes: dataPanel;
|
|
left: var(--spacing-l);
|
|
|
|
> * {
|
|
border-radius: var(--border-radius-large) 0 0 var(--border-radius-large);
|
|
}
|
|
}
|
|
|
|
.outputPanel {
|
|
composes: dataPanel;
|
|
right: var(--spacing-l);
|
|
|
|
> * {
|
|
border-radius: 0 var(--border-radius-large) var(--border-radius-large) 0;
|
|
}
|
|
}
|
|
|
|
.mainPanel {
|
|
position: absolute;
|
|
height: 100%;
|
|
|
|
&:hover {
|
|
.draggable {
|
|
visibility: visible;
|
|
}
|
|
}
|
|
}
|
|
|
|
.mainPanelInner {
|
|
height: 100%;
|
|
border: var(--border-base);
|
|
border-radius: var(--border-radius-large);
|
|
box-shadow: 0 4px 16px rgb(50 61 85 / 10%);
|
|
overflow: hidden;
|
|
|
|
&.dragging {
|
|
border-color: var(--color-primary);
|
|
box-shadow: 0px 6px 16px rgba(255, 74, 51, 0.15);
|
|
}
|
|
}
|
|
|
|
.draggable {
|
|
visibility: hidden;
|
|
}
|
|
|
|
.dragButtonContainer {
|
|
position: absolute;
|
|
top: -12px;
|
|
width: 100%;
|
|
height: 12px;
|
|
display: flex;
|
|
justify-content: center;
|
|
pointer-events: none;
|
|
|
|
.draggable {
|
|
pointer-events: all;
|
|
}
|
|
&:hover .draggable {
|
|
visibility: visible;
|
|
}
|
|
}
|
|
|
|
.visible {
|
|
visibility: visible;
|
|
}
|
|
</style>
|