mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 12:44:07 -08:00
feat(editor): Add Workflow Stickies (Notes) (#3154)
* N8N-3029 Add Node Type for Wokrflow Stickies/Notes * N8N-3029 Update Content, Update Aliasses * N8N-3030 Created N8N Sticky Component in Design System * N8N-3030 Fixed Code spaccing Sticky Component * N8N-3030 Fixed Code spaccing StickyStories Component * N8N-3030 Fixed Code spaccing Markdown Component * N8N-3030 Added Sticky Colors Pallete into Storybook, Update Color Variables for Sticky Component * N8N-3030 Added Unfocus Event * N8N-3030 Update Default Placeholder, Markdown Styles, Fixed Edit State, Added Text to EditState, Fixed Height of Area, Turned off Resize of textarea * N8N-3030 Update Sticky Overflow, Update Hover States, Updated Markdown Overflow * N8N-3030, N8N-3031 - Add Resize to Sticky, Created N8n-Resize component * N8N-3031 Fixed Importing Components in Editor-ui * N8N-3031 Fixed Resize Component, Fixed Gradient * N8N-3030, N8N-3031 Update Note Description * N8N-3032 Hotfix Building Storybook * N8N-3032 - Select Behaviour, Changes in Resize Component, Emit on Width/Height/Top/Left Change * N8N-3032 Update Resize Component to emmit left/top, Update Dynamic Resize on Selected Background * N8N-3032 Updated / Dragging vs Resizing, prevent open Modal for stickies * N8N-3032 Added ID props to n8n-sticky // dynamic id for multi resizing in NodeView * N8N-3033 Add dynamic size Tooltip on Sticky * N8N-3033 Updated Z-index for Sticky Component * N8N-3033 Updated N8N-Resize Component, Fixed SelectedBackround for Sticky Component * N8N-3033 Refactor * N8N-3033 Focus/Defocus on TextArea * N8N-3033 Fixed Resizing on NW Point * N8N-3030 Save content in vuex on input change * N8N-3033 Fixed Resizer, Save Width and Height in Vue * N8N-3033 Hide Sticky Footer on small height/width * N8N-3033 Fixed Resizer * N8N-3033 Dynamic Z-index for Stickies * N8N-3033 Dynamic Z-index for Stickies * N8N-3033 Removed static z-index for select sticky class * N8N-3034 Added Telemetry * N8N-3030 Formatter * N8N-3030 Format code * N8N-3030 Fixed Selecting Stickies * N8N-3033 Fixed Notifications * N8N-3030 Added new paddings for Default Stickies * N8N-3033 Prevent Scrolling NodeView when Sticky is in Edit mode and Mouse is Over the TextArea * N8N-3030 Prevent double clicking to switch state of Sticky component in Edit Mode * N8N-3033 Fixed Z-index of Stickies * N8N-3033 Prevent delete node when in EditMode * N8N-3030 Prevent Delete Button to delete the Sticky while in Edit Mode * N8N-3030 Change EditMode (emit) on keyboard shortucts, update Markdown Links & Images, Added new props * N8N-3030 Sticky Component - No padding when hiding footer text * N8N-3033 Fix Resizing enter into Edit Mode * N8N-3033 Selecting different nodes - exit the edit mode * N8N-3033 Auto Select Text in text-area by default - Sticky Component * N8N-3033 Prevent Default behaviour for CTRL + X, CTRL + A when Sticky is Active && inEditMode * N8N-3033 Refactor Resizer, Refactor Sticky, Update zIndex inEditMode * N8N-3033 Updated Default Text // Node-base, Storybook * N8N-3033 Add Resizing in EditMode - Components update * N8N-3033 Fixed Footer - Show/Hide on Resize in EditMode * N8N-3033 Fix ActiveSticky on Init * N8N-3033 Refactor Sticky in Vuex, Fixed Init Sticky Tweaks, Prevent Modal Openning, Save on Keyboard shortcuts * Stickies - Update Note node with new props * N8N-3030 Updated Default Note text, Update the Markdown Link * N8N-3030 CMD-C does not copy the text fix * N8N-3030 Fix Max Zoom / Zoom out shortcuts disabled in editState * N8N-3030 Z-index fixed during Edit Mode typing * N8N-3030 Prevent Autoselect Text in Stickies if the text is not default * N8N-3030 Fixed ReadOnly Bugs / Prevent showing Tooltip, Resizing * N8N-3030 Added Sticky Creator Button * N8N-3030 Update Icon / Sticky Creator Button * N8N-3033 Update Sticky Icon / StickyCreator Button * update package lock * 🔩 update note props * 🚿 clean props * 🔧 linting * 🔧 fix spacing * remove resize component * remove resize component * ✂ clean up sticky * revert back to height width * revert back to height/width * replace zindex property * replace default text property * use i18n to translate * update package lock * move resize * clean up how height/width are set * fix resize for sticky to support left/top * clean up resize * fix lasso/highlight bug * remove unused props * fix zoom to fit * fix padding for demo view * fix readonly * remove iseditable, use active state * clean up keyboard events * chang button size, no edit on insert * scale resizing correctly * make active on resize * fix select on resize/move * use outline icon * allow for multiple line breaks * fix multi line bug * fix edit mode outline * keep edit open as one resizes * respect multiple spaces * fix scrolling bug * clean up hover impl * clean up references to note * disable for rename * fix drifting while drag * fix mouse cursor on resize * fix sticky min height * refactor resize into component * fix pulling too far bug * fix delete/cut all bug * fix padding bottom * fix active change on resize * add transition to button * Fix sticky markdown click * add solid fa icon * update node graph, telemetry event * add snapping * change alt text * update package lock * fix bug in button hover * add back transition * clean up resize * add grid size as param * remove breaks * clean up markdown * lint fixes * fix spacing * clean up markdown colors * clean up classes in resize * clean up resize * update sticky story * fix spacing * clean up classes * revert change * revert change * revert change * clean up sticky component * remove unused component * remove unnessary data * remove unnessary data * clean up actions * clean up sticky size * clean up unnessary border style * fix bug * replace sticky note name * update description * remove support for multi spaces * update tracking name * update telemetry reqs * fix enter bug * update alt text * update sticky notes doc url * fix readonly bug * update class name * update quote marks Co-authored-by: SchnapsterDog <olivertrajceski@yahoo.com>
This commit is contained in:
parent
d446f9e281
commit
31dd01f9cb
75123
package-lock.json
generated
75123
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -84,11 +84,18 @@ export class InternalHooksClass implements IInternalHooksClass {
|
||||||
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
|
async onWorkflowSaved(userId: string, workflow: IWorkflowDb): Promise<void> {
|
||||||
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
const { nodeGraph } = TelemetryHelpers.generateNodesGraph(workflow, this.nodeTypes);
|
||||||
|
|
||||||
|
const notesCount = Object.keys(nodeGraph.notes).length;
|
||||||
|
const overlappingCount = Object.values(nodeGraph.notes).filter(
|
||||||
|
(note) => note.overlapping,
|
||||||
|
).length;
|
||||||
|
|
||||||
return this.telemetry.track('User saved workflow', {
|
return this.telemetry.track('User saved workflow', {
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
workflow_id: workflow.id,
|
workflow_id: workflow.id,
|
||||||
node_graph: nodeGraph,
|
node_graph: nodeGraph,
|
||||||
node_graph_string: JSON.stringify(nodeGraph),
|
node_graph_string: JSON.stringify(nodeGraph),
|
||||||
|
notes_count_overlapping: overlappingCount,
|
||||||
|
notes_count_non_overlapping: notesCount - overlappingCount,
|
||||||
version_cli: this.versionCli,
|
version_cli: this.versionCli,
|
||||||
num_tags: workflow.tags?.length ?? 0,
|
num_tags: workflow.tags?.length ?? 0,
|
||||||
});
|
});
|
||||||
|
|
|
@ -79,6 +79,7 @@
|
||||||
"vue-loader": "^15.9.7",
|
"vue-loader": "^15.9.7",
|
||||||
"vue-property-decorator": "^9.1.2",
|
"vue-property-decorator": "^9.1.2",
|
||||||
"vue-template-compiler": "^2.6.11",
|
"vue-template-compiler": "^2.6.11",
|
||||||
|
"vue-typed-mixins": "^0.2.0",
|
||||||
"vue2-boring-avatars": "0.3.4",
|
"vue2-boring-avatars": "0.3.4",
|
||||||
"xss": "^1.0.10"
|
"xss": "^1.0.10"
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,6 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
|
||||||
},
|
},
|
||||||
round: {
|
round: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
|
|
@ -26,7 +26,6 @@ export default {
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
|
|
|
@ -40,7 +40,6 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
icon: {
|
icon: {
|
||||||
type: String,
|
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-if="!loading" ref="editor" :class="$style.markdown" v-html="htmlContent" />
|
<div
|
||||||
|
v-if="!loading"
|
||||||
|
ref="editor"
|
||||||
|
:class="$style[theme]" v-html="htmlContent"
|
||||||
|
/>
|
||||||
<div v-else :class="$style.markdown">
|
<div v-else :class="$style.markdown">
|
||||||
<div v-for="(block, index) in loadingBlocks"
|
<div v-for="(block, index) in loadingBlocks"
|
||||||
:key="index">
|
:key="index">
|
||||||
|
@ -59,6 +63,9 @@ export default {
|
||||||
content: {
|
content: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
withMultiBreaks: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
images: {
|
images: {
|
||||||
type: Array,
|
type: Array,
|
||||||
},
|
},
|
||||||
|
@ -75,6 +82,10 @@ export default {
|
||||||
return 3;
|
return 3;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
type: String,
|
||||||
|
default: 'markdown',
|
||||||
|
},
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default() {
|
default() {
|
||||||
|
@ -106,7 +117,11 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
||||||
const html = this.md.render(escapeMarkdown(this.content));
|
let contentToRender = this.content;
|
||||||
|
if (this.withMultiBreaks) {
|
||||||
|
contentToRender = contentToRender.replaceAll('\n\n', '\n \n');
|
||||||
|
}
|
||||||
|
const html = this.md.render(escapeMarkdown(contentToRender));
|
||||||
const safeHtml = xss(html, {
|
const safeHtml = xss(html, {
|
||||||
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
||||||
if (tag === 'img' && name === 'src') {
|
if (tag === 'img' && name === 'src') {
|
||||||
|
@ -214,6 +229,67 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sticky {
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
|
||||||
|
h1, h2, h3, h4 {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3, h4, h5, h6 {
|
||||||
|
font-size: var(--font-size-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--font-line-height-loose);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul, ol {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
padding-left: var(--spacing-m);
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin-top: 0.25em;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
line-height: var(--font-line-height-regular);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
background-color: var(--color-background-base);
|
||||||
|
padding: 0 var(--spacing-4xs);
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
pre > code,li > code, p > code {
|
||||||
|
color: var(--color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.spacer {
|
.spacer {
|
||||||
margin: var(--spacing-2xl);
|
margin: var(--spacing-2xl);
|
||||||
}
|
}
|
||||||
|
|
238
packages/design-system/src/components/N8nSticky/Resize.vue
Normal file
238
packages/design-system/src/components/N8nSticky/Resize.vue
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
<template>
|
||||||
|
<div :class="$style.resize">
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="right" :class="[$style.resizer, $style.right]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="left" :class="[$style.resizer, $style.left]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top" :class="[$style.resizer, $style.top]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom" :class="[$style.resizer, $style.bottom]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-left" :class="[$style.resizer, $style.topLeft]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="top-right" :class="[$style.resizer, $style.topRight]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-left" :class="[$style.resizer, $style.bottomLeft]" />
|
||||||
|
<div v-if="isResizingEnabled" @mousedown="resizerMove" data-dir="bottom-right" :class="[$style.resizer, $style.bottomRight]" />
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
const cursorMap = {
|
||||||
|
right: 'ew-resize',
|
||||||
|
top: 'ns-resize',
|
||||||
|
bottom: 'ns-resize',
|
||||||
|
left: 'ew-resize',
|
||||||
|
'top-left': 'nw-resize',
|
||||||
|
'top-right' : 'ne-resize',
|
||||||
|
'bottom-left': 'sw-resize',
|
||||||
|
'bottom-right': 'se-resize',
|
||||||
|
};
|
||||||
|
|
||||||
|
function closestNumber(value: number, divisor: number): number {
|
||||||
|
let q = parseInt(value / divisor);
|
||||||
|
let n1 = divisor * q;
|
||||||
|
|
||||||
|
let n2 = (value * divisor) > 0 ?
|
||||||
|
(divisor * (q + 1)) : (divisor * (q - 1));
|
||||||
|
|
||||||
|
if (Math.abs(value - n1) < Math.abs(value - n2))
|
||||||
|
return n1;
|
||||||
|
|
||||||
|
return n2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSize(delta, min, virtual, gridSize): number {
|
||||||
|
const target = closestNumber(virtual, gridSize);
|
||||||
|
if (target >= min && virtual > 0) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
return min;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'n8n-resize',
|
||||||
|
props: {
|
||||||
|
isResizingEnabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
gridSize: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
dir: '',
|
||||||
|
dHeight: 0,
|
||||||
|
dWidth: 0,
|
||||||
|
vHeight: 0,
|
||||||
|
vWidth: 0,
|
||||||
|
x: 0,
|
||||||
|
y: 0,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
resizerMove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
const targetResizer = e.target;
|
||||||
|
this.dir = targetResizer.dataset.dir;
|
||||||
|
document.body.style.cursor = cursorMap[this.dir];
|
||||||
|
|
||||||
|
this.x = e.pageX;
|
||||||
|
this.y = e.pageY;
|
||||||
|
this.dWidth = 0;
|
||||||
|
this.dHeight = 0;
|
||||||
|
this.vHeight = this.height;
|
||||||
|
this.vWidth = this.width;
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', this.mouseMove);
|
||||||
|
window.addEventListener('mouseup', this.mouseUp);
|
||||||
|
this.$emit('resizestart');
|
||||||
|
},
|
||||||
|
mouseMove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
let dWidth = 0;
|
||||||
|
let dHeight = 0;
|
||||||
|
let top = false;
|
||||||
|
let left = false;
|
||||||
|
|
||||||
|
if (this.dir.includes('right')) {
|
||||||
|
dWidth = e.pageX - this.x;
|
||||||
|
}
|
||||||
|
if (this.dir.includes('left')) {
|
||||||
|
dWidth = this.x - e.pageX;
|
||||||
|
left = true;
|
||||||
|
}
|
||||||
|
if (this.dir.includes('top')) {
|
||||||
|
dHeight = this.y - e.pageY;
|
||||||
|
top = true;
|
||||||
|
}
|
||||||
|
if (this.dir.includes('bottom')) {
|
||||||
|
dHeight = e.pageY - this.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const deltaWidth = (dWidth - this.dWidth) / this.scale;
|
||||||
|
const deltaHeight = (dHeight - this.dHeight) / this.scale;
|
||||||
|
|
||||||
|
this.vHeight = this.vHeight + deltaHeight;
|
||||||
|
this.vWidth = this.vWidth + deltaWidth;
|
||||||
|
const height = getSize(deltaHeight, this.minHeight, this.vHeight, this.gridSize);
|
||||||
|
const width = getSize(deltaWidth, this.minWidth, this.vWidth, this.gridSize);
|
||||||
|
|
||||||
|
const dX = left && width !== this.width ? -1 * (width - this.width) : 0;
|
||||||
|
const dY = top && height !== this.height ? -1 * (height - this.height): 0;
|
||||||
|
|
||||||
|
this.$emit('resize', { height, width, dX, dY });
|
||||||
|
this.dHeight = dHeight;
|
||||||
|
this.dWidth = dWidth;
|
||||||
|
},
|
||||||
|
mouseUp(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
this.$emit('resizeend');
|
||||||
|
window.removeEventListener('mousemove', this.mouseMove);
|
||||||
|
window.removeEventListener('mouseup', this.mouseUp);
|
||||||
|
document.body.style.cursor = 'unset';
|
||||||
|
this.dir = '';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.resize {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.resizer {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
top: -2px;
|
||||||
|
right: -2px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.top {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottom {
|
||||||
|
width: 100%;
|
||||||
|
height: 12px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -2px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
width: 12px;
|
||||||
|
height: 100%;
|
||||||
|
top: -2px;
|
||||||
|
left: -2px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topLeft {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: -3px;
|
||||||
|
left: -3px;
|
||||||
|
cursor: nw-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topRight {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
top: -3px;
|
||||||
|
right: -3px;
|
||||||
|
cursor: ne-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomLeft {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
bottom: -3px;
|
||||||
|
left: -3px;
|
||||||
|
cursor: sw-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomRight {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
bottom: -3px;
|
||||||
|
right: -3px;
|
||||||
|
cursor: se-resize;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,67 @@
|
||||||
|
import { action } from '@storybook/addon-actions';
|
||||||
|
import N8nSticky from './Sticky.vue';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'Atoms/Sticky',
|
||||||
|
component: N8nSticky,
|
||||||
|
argTypes: {
|
||||||
|
content: {
|
||||||
|
control: {
|
||||||
|
control: 'text',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
control: {
|
||||||
|
control: 'Boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
control: {
|
||||||
|
control: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const methods = {
|
||||||
|
onInput: action('input'),
|
||||||
|
onResize: action('resize'),
|
||||||
|
onResizeEnd: action('resizeend'),
|
||||||
|
onResizeStart: action('resizestart'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Template = (args, { argTypes }) => ({
|
||||||
|
props: Object.keys(argTypes),
|
||||||
|
components: {
|
||||||
|
N8nSticky,
|
||||||
|
},
|
||||||
|
template:
|
||||||
|
'<n8n-sticky v-bind="$props" @resize="onResize" @resizeend="onResizeEnd" @resizeStart="onResizeStart" @input="onInput"></n8n-sticky>',
|
||||||
|
methods,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const Sticky = Template.bind({});
|
||||||
|
Sticky.args = {
|
||||||
|
height: 160,
|
||||||
|
width: 150,
|
||||||
|
content: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
|
||||||
|
defaultText: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
|
||||||
|
minHeight: 80,
|
||||||
|
minWidth: 150,
|
||||||
|
readOnly: false,
|
||||||
|
};
|
253
packages/design-system/src/components/N8nSticky/Sticky.vue
Normal file
253
packages/design-system/src/components/N8nSticky/Sticky.vue
Normal file
|
@ -0,0 +1,253 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{[$style.sticky]: true, [$style.clickable]: !isResizing}"
|
||||||
|
:style="styles"
|
||||||
|
@keydown.prevent
|
||||||
|
>
|
||||||
|
<resize
|
||||||
|
:isResizingEnabled="!readOnly"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
:minHeight="minHeight"
|
||||||
|
:minWidth="minWidth"
|
||||||
|
:scale="scale"
|
||||||
|
:gridSize="gridSize"
|
||||||
|
@resizeend="onResizeEnd"
|
||||||
|
@resize="onResize"
|
||||||
|
@resizestart="onResizeStart"
|
||||||
|
>
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-show="!editMode"
|
||||||
|
:class="$style.wrapper"
|
||||||
|
@dblclick.stop="onDoubleClick"
|
||||||
|
>
|
||||||
|
<n8n-markdown
|
||||||
|
theme="sticky"
|
||||||
|
:content="content"
|
||||||
|
:withMultiBreaks="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-show="editMode"
|
||||||
|
@click.stop
|
||||||
|
@mousedown.stop
|
||||||
|
@mouseup.stop
|
||||||
|
@keydown.esc="onInputBlur"
|
||||||
|
@keydown.stop
|
||||||
|
@wheel.stop
|
||||||
|
class="sticky-textarea"
|
||||||
|
:class="{'full-height': !shouldShowFooter}"
|
||||||
|
>
|
||||||
|
<n8n-input
|
||||||
|
:value="content"
|
||||||
|
type="textarea"
|
||||||
|
:rows="5"
|
||||||
|
@blur="onInputBlur"
|
||||||
|
@input="onInput"
|
||||||
|
ref="input"
|
||||||
|
/>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
|
||||||
|
<n8n-text
|
||||||
|
size="xsmall"
|
||||||
|
aligh="right"
|
||||||
|
>
|
||||||
|
<span v-html="t('sticky.markdownHint')"></span>
|
||||||
|
</n8n-text>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</resize>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import N8nInput from '../N8nInput';
|
||||||
|
import N8nMarkdown from '../N8nMarkdown';
|
||||||
|
import Resize from './Resize';
|
||||||
|
import N8nText from '../N8nText';
|
||||||
|
import Locale from '../../mixins/locale';
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
|
export default mixins(Locale).extend({
|
||||||
|
name: 'n8n-sticky',
|
||||||
|
props: {
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 180,
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 240,
|
||||||
|
},
|
||||||
|
minHeight: {
|
||||||
|
type: Number,
|
||||||
|
default: 80,
|
||||||
|
},
|
||||||
|
minWidth: {
|
||||||
|
type: Number,
|
||||||
|
default: 150,
|
||||||
|
},
|
||||||
|
scale: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
gridSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 20,
|
||||||
|
},
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
default: '0',
|
||||||
|
},
|
||||||
|
defaultText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
editMode: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
N8nInput,
|
||||||
|
N8nMarkdown,
|
||||||
|
Resize,
|
||||||
|
N8nText,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
isResizing: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
resHeight(): number {
|
||||||
|
if (this.height < this.minHeight) {
|
||||||
|
return this.minHeight;
|
||||||
|
}
|
||||||
|
return this.height;
|
||||||
|
},
|
||||||
|
resWidth(): number {
|
||||||
|
if (this.width < this.minWidth) {
|
||||||
|
return this.minWidth;
|
||||||
|
}
|
||||||
|
return this.width;
|
||||||
|
},
|
||||||
|
styles() {
|
||||||
|
return {
|
||||||
|
height: this.resHeight + 'px',
|
||||||
|
width: this.resWidth + 'px',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
shouldShowFooter() {
|
||||||
|
return this.resHeight > 100 && this.resWidth > 155;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
onDoubleClick() {
|
||||||
|
if (!this.readOnly) {
|
||||||
|
this.$emit('edit', true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInputBlur(value) {
|
||||||
|
if (!this.isResizing) {
|
||||||
|
this.$emit('edit', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInput(value: string) {
|
||||||
|
this.$emit('input', value);
|
||||||
|
},
|
||||||
|
onResize(values) {
|
||||||
|
this.$emit('resize', values);
|
||||||
|
},
|
||||||
|
onResizeEnd(resizeEnd) {
|
||||||
|
this.isResizing = false;
|
||||||
|
this.$emit('resizeend', resizeEnd);
|
||||||
|
},
|
||||||
|
onResizeStart() {
|
||||||
|
this.isResizing = true;
|
||||||
|
this.$emit('resizestart');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
editMode(newMode, prevMode) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (newMode && !prevMode && this.$refs.input && this.$refs.input.$refs && this.$refs.input.$refs.textarea) {
|
||||||
|
const textarea = this.$refs.input.$refs.textarea;
|
||||||
|
if (this.defaultText === this.content) {
|
||||||
|
textarea.select();
|
||||||
|
}
|
||||||
|
textarea.focus();
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.sticky {
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--color-sticky-default-background);
|
||||||
|
border: 1px solid var(--color-sticky-default-border);
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.clickable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrapper {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
padding: var(--spacing-2xs) var(--spacing-xs) 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
position: absolute;
|
||||||
|
background: linear-gradient(180deg, var(--color-sticky-default-background), #fff5d600 0.01%, var(--color-sticky-default-background));
|
||||||
|
border-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
padding: var(--spacing-5xs) var(--spacing-2xs) 0 var(--spacing-2xs);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.sticky-textarea {
|
||||||
|
height: calc(100% - var(--spacing-l));
|
||||||
|
padding: var(--spacing-2xs) var(--spacing-2xs) 0 var(--spacing-2xs);
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
|
.el-textarea {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.el-textarea__inner {
|
||||||
|
height: 100%;
|
||||||
|
resize: unset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.full-height {
|
||||||
|
height: calc(100% - var(--spacing-2xs));
|
||||||
|
}
|
||||||
|
</style>
|
3
packages/design-system/src/components/N8nSticky/index.js
Normal file
3
packages/design-system/src/components/N8nSticky/index.js
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
import Sticky from './Sticky.vue';
|
||||||
|
|
||||||
|
export default Sticky;
|
|
@ -57,6 +57,7 @@ import N8nOption from './N8nOption';
|
||||||
import N8nRadioButtons from './N8nRadioButtons';
|
import N8nRadioButtons from './N8nRadioButtons';
|
||||||
import N8nSelect from './N8nSelect';
|
import N8nSelect from './N8nSelect';
|
||||||
import N8nSpinner from './N8nSpinner';
|
import N8nSpinner from './N8nSpinner';
|
||||||
|
import N8nSticky from './N8nSticky';
|
||||||
import N8nSquareButton from './N8nSquareButton';
|
import N8nSquareButton from './N8nSquareButton';
|
||||||
import N8nTags from './N8nTags';
|
import N8nTags from './N8nTags';
|
||||||
import N8nTabs from './N8nTabs';
|
import N8nTabs from './N8nTabs';
|
||||||
|
@ -93,6 +94,7 @@ export {
|
||||||
N8nRadioButtons,
|
N8nRadioButtons,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nSticky,
|
||||||
N8nSquareButton,
|
N8nSquareButton,
|
||||||
N8nTabs,
|
N8nTabs,
|
||||||
N8nTags,
|
N8nTags,
|
||||||
|
|
|
@ -16,4 +16,5 @@ export default {
|
||||||
config.minimum > 1 ? 's' : ''
|
config.minimum > 1 ? 's' : ''
|
||||||
}`),
|
}`),
|
||||||
"formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter",
|
"formInput.validator.defaultPasswordRequirements": "8+ characters, at least 1 number and 1 capital letter",
|
||||||
|
"sticky.markdownHint": `You can style with <a href="https://docs.n8n.io/workflows/sticky-notes/" target="_blank">Markdown</a>`,
|
||||||
};
|
};
|
||||||
|
|
|
@ -166,3 +166,17 @@ import ColorCircles from './ColorCircles.vue';
|
||||||
}}
|
}}
|
||||||
</Story>
|
</Story>
|
||||||
</Canvas>
|
</Canvas>
|
||||||
|
|
||||||
|
## Sticky
|
||||||
|
|
||||||
|
<Canvas>
|
||||||
|
<Story name="sticky">
|
||||||
|
{{
|
||||||
|
template: `<color-circles :colors="['--color-sticky-default-background', '--color-sticky-default-border']" />`,
|
||||||
|
components: {
|
||||||
|
ColorCircles,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
</Story>
|
||||||
|
</Canvas>
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ export const escapeMarkdown = (html: string | undefined): string => {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
const escaped = html.replace(/</g, "<").replace(/>/g, ">");
|
const escaped = html.replace(/</g, "<").replace(/>/g, ">");
|
||||||
|
|
||||||
// unescape greater than quotes at start of line
|
// unescape greater than quotes at start of line
|
||||||
const withQuotes = escaped.replace(/^((\s)*(>)+)+\s*/gm, (matches) => {
|
const withQuotes = escaped.replace(/^((\s)*(>)+)+\s*/gm, (matches) => {
|
||||||
return matches.replace(/>/g, '>');
|
return matches.replace(/>/g, '>');
|
||||||
|
|
|
@ -70,13 +70,6 @@
|
||||||
var(--color-secondary-l)
|
var(--color-secondary-l)
|
||||||
);
|
);
|
||||||
|
|
||||||
--color-secondary-tint-1-l: 92%;
|
|
||||||
--color-secondary-tint-1: hsl(
|
|
||||||
var(--color-secondary-h),
|
|
||||||
var(--color-secondary-s),
|
|
||||||
var(--color-secondary-tint-1-l)
|
|
||||||
);
|
|
||||||
|
|
||||||
--color-success-h: 150.4;
|
--color-success-h: 150.4;
|
||||||
--color-success-s: 60%;
|
--color-success-s: 60%;
|
||||||
--color-success-l: 40.4%;
|
--color-success-l: 40.4%;
|
||||||
|
@ -340,6 +333,24 @@
|
||||||
--color-json-line: #bfcbd9;
|
--color-json-line: #bfcbd9;
|
||||||
--color-json-highlight: #E2E5EE;
|
--color-json-highlight: #E2E5EE;
|
||||||
|
|
||||||
|
--color-sticky-default-background-h: 46;
|
||||||
|
--color-sticky-default-background-s: 100%;
|
||||||
|
--color-sticky-default-background-l: 92%;
|
||||||
|
--color-sticky-default-background: hsl(
|
||||||
|
var(--color-sticky-default-background-h),
|
||||||
|
var(--color-sticky-default-background-s),
|
||||||
|
var(--color-sticky-default-background-l)
|
||||||
|
);
|
||||||
|
|
||||||
|
--color-sticky-default-border-h: 43;
|
||||||
|
--color-sticky-default-border-s: 75%;
|
||||||
|
--color-sticky-default-border-l: 80%;
|
||||||
|
--color-sticky-default-border: hsl(
|
||||||
|
var(--color-sticky-default-border-h),
|
||||||
|
var(--color-sticky-default-border-s),
|
||||||
|
var(--color-sticky-default-border-l)
|
||||||
|
);
|
||||||
|
|
||||||
--border-radius-xlarge: 12px;
|
--border-radius-xlarge: 12px;
|
||||||
--border-radius-large: 8px;
|
--border-radius-large: 8px;
|
||||||
--border-radius-base: 4px;
|
--border-radius-base: 4px;
|
||||||
|
|
|
@ -25,14 +25,15 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^4.5.0",
|
"@fontsource/open-sans": "^4.5.0",
|
||||||
|
"@fortawesome/free-regular-svg-icons": "^6.1.1",
|
||||||
"luxon": "^2.3.0",
|
"luxon": "^2.3.0",
|
||||||
"n8n-design-system": "~0.17.0",
|
|
||||||
"monaco-editor": "^0.29.1",
|
"monaco-editor": "^0.29.1",
|
||||||
|
"n8n-design-system": "~0.17.0",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-fragment": "^1.5.2",
|
"vue-fragment": "^1.5.2",
|
||||||
"vue2-boring-avatars": "0.3.4",
|
|
||||||
"vue-i18n": "^8.26.7",
|
"vue-i18n": "^8.26.7",
|
||||||
|
"vue2-boring-avatars": "0.3.4",
|
||||||
"xss": "^1.0.10"
|
"xss": "^1.0.10"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<el-dialog
|
<el-dialog
|
||||||
:visible="!!node || renaming"
|
:visible="(!!node || renaming) && !isActiveStickyNode"
|
||||||
:before-close="close"
|
:before-close="close"
|
||||||
:show-close="false"
|
:show-close="false"
|
||||||
custom-class="data-display-wrapper"
|
custom-class="data-display-wrapper"
|
||||||
|
@ -41,6 +41,7 @@ import RunData from '@/components/RunData.vue';
|
||||||
import mixins from 'vue-typed-mixins';
|
import mixins from 'vue-typed-mixins';
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import { STICKY_NODE_TYPE } from '@/constants';
|
||||||
|
|
||||||
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
name: 'DataDisplay',
|
name: 'DataDisplay',
|
||||||
|
@ -76,15 +77,18 @@ export default mixins(externalHooks, nodeHelpers, workflowHelpers).extend({
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
isActiveStickyNode(): boolean {
|
||||||
|
return !!this.$store.getters.activeNode && this.$store.getters.activeNode.type === STICKY_NODE_TYPE;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
node (node, oldNode) {
|
node (node, oldNode) {
|
||||||
if(node && !oldNode) {
|
if(node && !oldNode && !this.isActiveStickyNode) {
|
||||||
this.triggerWaitingWarningEnabled = false;
|
this.triggerWaitingWarningEnabled = false;
|
||||||
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
this.$externalHooks().run('dataDisplay.nodeTypeChanged', { nodeSubtitle: this.getNodeSubtitle(node, this.nodeType, this.getWorkflow()) });
|
||||||
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.track('User opened node modal', { node_type: this.nodeType ? this.nodeType.name : '', workflow_id: this.$store.getters.workflowId });
|
||||||
}
|
}
|
||||||
if (window.top) {
|
if (window.top && !this.isActiveStickyNode) {
|
||||||
window.top.postMessage(JSON.stringify({command: (node? 'openNDV': 'closeNDV')}), '*');
|
window.top.postMessage(JSON.stringify({command: (node? 'openNDV': 'closeNDV')}), '*');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
288
packages/editor-ui/src/components/Sticky.vue
Normal file
288
packages/editor-ui/src/components/Sticky.vue
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
<template>
|
||||||
|
<div class="sticky-wrapper" :style="stickyPosition">
|
||||||
|
<div
|
||||||
|
:class="{'sticky-default': true, 'touch-active': isTouchActive, 'is-touch-device': isTouchDevice}"
|
||||||
|
:style="stickySize"
|
||||||
|
>
|
||||||
|
<div class="select-sticky-background" v-show="isSelected" />
|
||||||
|
<div
|
||||||
|
class="sticky-box"
|
||||||
|
:data-name="data.name"
|
||||||
|
:ref="data.name"
|
||||||
|
@click.left="mouseLeftClick"
|
||||||
|
v-touch:start="touchStart"
|
||||||
|
v-touch:end="touchEnd"
|
||||||
|
>
|
||||||
|
<n8n-sticky
|
||||||
|
:content.sync="node.parameters.content"
|
||||||
|
:height="node.parameters.height"
|
||||||
|
:width="node.parameters.width"
|
||||||
|
:scale="nodeViewScale"
|
||||||
|
:id="nodeIndex"
|
||||||
|
:readOnly="isReadOnly"
|
||||||
|
:defaultText="defaultText"
|
||||||
|
:editMode="isActive && !isReadOnly"
|
||||||
|
:gridSize="gridSize"
|
||||||
|
@input="onInputChange"
|
||||||
|
@edit="onEdit"
|
||||||
|
@resizestart="onResizeStart"
|
||||||
|
@resize="onResize"
|
||||||
|
@resizeend="onResizeEnd"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-show="showActions" class="sticky-options no-select-on-click">
|
||||||
|
<div v-touch:tap="deleteNode" class="option" :title="$locale.baseText('node.deleteNode')" >
|
||||||
|
<font-awesome-icon icon="trash" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
import mixins from 'vue-typed-mixins';
|
||||||
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
|
import { nodeBase } from '@/components/mixins/nodeBase';
|
||||||
|
import { nodeHelpers } from '@/components/mixins/nodeHelpers';
|
||||||
|
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
|
||||||
|
import { getStyleTokenValue, isNumber, isString } from './helpers';
|
||||||
|
import { INodeUi, XYPosition } from '@/Interface';
|
||||||
|
|
||||||
|
import {
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
export default mixins(externalHooks, nodeBase, nodeHelpers, workflowHelpers).extend({
|
||||||
|
name: 'Sticky',
|
||||||
|
props: {
|
||||||
|
nodeViewScale: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
gridSize: {
|
||||||
|
type: Number,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
defaultText (): string {
|
||||||
|
if (!this.nodeType) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
const properties = this.nodeType.properties;
|
||||||
|
const content = properties.find((property) => property.name === 'content');
|
||||||
|
|
||||||
|
return content && isString(content.default) ? content.default : '';
|
||||||
|
},
|
||||||
|
isSelected (): boolean {
|
||||||
|
return this.$store.getters.getSelectedNodes.find((node: INodeUi) => node.name === this.data.name);
|
||||||
|
},
|
||||||
|
nodeType (): INodeTypeDescription | null {
|
||||||
|
return this.data && this.$store.getters.nodeType(this.data.type, this.data.typeVersion);
|
||||||
|
},
|
||||||
|
node (): INodeUi | undefined { // same as this.data but reactive..
|
||||||
|
return this.$store.getters.nodesByName[this.name] as INodeUi | undefined;
|
||||||
|
},
|
||||||
|
position (): XYPosition {
|
||||||
|
if (this.node) {
|
||||||
|
return this.node.position;
|
||||||
|
} else {
|
||||||
|
return [0, 0];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
height(): number {
|
||||||
|
return this.node && isNumber(this.node.parameters.height)? this.node.parameters.height : 0;
|
||||||
|
},
|
||||||
|
width(): number {
|
||||||
|
return this.node && isNumber(this.node.parameters.width)? this.node.parameters.width : 0;
|
||||||
|
},
|
||||||
|
stickySize(): object {
|
||||||
|
const returnStyles: {
|
||||||
|
[key: string]: string | number;
|
||||||
|
} = {
|
||||||
|
height: this.height + 'px',
|
||||||
|
width: this.width + 'px',
|
||||||
|
};
|
||||||
|
|
||||||
|
return returnStyles;
|
||||||
|
},
|
||||||
|
stickyPosition (): object {
|
||||||
|
const returnStyles: {
|
||||||
|
[key: string]: string | number;
|
||||||
|
} = {
|
||||||
|
left: this.position[0] + 'px',
|
||||||
|
top: this.position[1] + 'px',
|
||||||
|
zIndex: this.isActive ? 9999999 : -1 * Math.floor((this.height * this.width) / 1000),
|
||||||
|
};
|
||||||
|
|
||||||
|
return returnStyles;
|
||||||
|
},
|
||||||
|
showActions(): boolean {
|
||||||
|
return !(this.hideActions || this.isReadOnly || this.workflowRunning || this.isResizing);
|
||||||
|
},
|
||||||
|
workflowRunning (): boolean {
|
||||||
|
return this.$store.getters.isActionActive('workflowRunning');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
isResizing: false,
|
||||||
|
isTouchActive: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
deleteNode () {
|
||||||
|
Vue.nextTick(() => {
|
||||||
|
// Wait a tick else vue causes problems because the data is gone
|
||||||
|
this.$emit('removeNode', this.data.name);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onEdit(edit: boolean) {
|
||||||
|
if (edit && !this.isActive && this.node) {
|
||||||
|
this.$store.commit('setActiveNode', this.node.name);
|
||||||
|
}
|
||||||
|
else if (this.isActive && !edit) {
|
||||||
|
this.$store.commit('setActiveNode', null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onInputChange(content: string) {
|
||||||
|
this.setParameters({content});
|
||||||
|
},
|
||||||
|
onResizeStart() {
|
||||||
|
this.isResizing = true;
|
||||||
|
if (!this.isSelected && this.node) {
|
||||||
|
this.$emit('nodeSelected', this.node.name, false, true);
|
||||||
|
}
|
||||||
|
const nodeIndex = this.$store.getters.getNodeIndex(this.data.name);
|
||||||
|
const nodeIdName = `node-${nodeIndex}`;
|
||||||
|
this.instance.destroyDraggable(nodeIdName); // todo
|
||||||
|
},
|
||||||
|
onResize({height, width, dX, dY}: { width: number, height: number, dX: number, dY: number }) {
|
||||||
|
if (!this.node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (dX !== 0 || dY !== 0) {
|
||||||
|
this.setPosition([this.node.position[0] + (dX || 0), this.node.position[1] + (dY || 0)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setParameters({ height, width });
|
||||||
|
},
|
||||||
|
onResizeEnd() {
|
||||||
|
this.isResizing = false;
|
||||||
|
this.__makeInstanceDraggable(this.data);
|
||||||
|
},
|
||||||
|
setParameters(params: {content?: string, height?: number, width?: number}) {
|
||||||
|
if (this.node) {
|
||||||
|
const nodeParameters = {
|
||||||
|
content: isString(params.content) ? params.content : this.node.parameters.content,
|
||||||
|
height: isNumber(params.height) ? params.height : this.node.parameters.height,
|
||||||
|
width: isNumber(params.width) ? params.width : this.node.parameters.width,
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateInformation = {
|
||||||
|
name: this.node.name,
|
||||||
|
value: nodeParameters,
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.commit('setNodeParameters', updateInformation);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setPosition(position: XYPosition) {
|
||||||
|
if (!this.node) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateInformation = {
|
||||||
|
name: this.node.name,
|
||||||
|
properties: {
|
||||||
|
position,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.$store.commit('updateNodeProperties', updateInformation);
|
||||||
|
},
|
||||||
|
touchStart () {
|
||||||
|
if (this.isTouchDevice === true && this.isMacOs === false && this.isTouchActive === false) {
|
||||||
|
this.isTouchActive = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.isTouchActive = false;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.sticky-wrapper {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.sticky-default {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
.sticky-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.touch-active,
|
||||||
|
&:hover {
|
||||||
|
.sticky-options {
|
||||||
|
display: flex;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sticky-options {
|
||||||
|
display: none;
|
||||||
|
justify-content: flex-start;
|
||||||
|
position: absolute;
|
||||||
|
top: -25px;
|
||||||
|
left: -8px;
|
||||||
|
height: 26px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-align: left;
|
||||||
|
z-index: 10;
|
||||||
|
color: #aaa;
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.option {
|
||||||
|
width: 28px;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.touch {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $--color-primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-touch-device .sticky-options {
|
||||||
|
left: -25px;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
.option.touch {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-sticky-background {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
background-color: hsla(var(--color-foreground-base-h), var(--color-foreground-base-s), var(--color-foreground-base-l), 60%);
|
||||||
|
border-radius: var(--border-radius-xlarge);
|
||||||
|
overflow: hidden;
|
||||||
|
height: calc(100% + 16px);
|
||||||
|
width: calc(100% + 16px);
|
||||||
|
left: -8px;
|
||||||
|
top: -8px;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -59,3 +59,11 @@ export function filterTemplateNodes(nodes: ITemplatesNode[]) {
|
||||||
export function setPageTitle(title: string) {
|
export function setPageTitle(title: string) {
|
||||||
window.document.title = title;
|
window.document.title = title;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isString(value: unknown): value is string {
|
||||||
|
return typeof value === 'string';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isNumber(value: unknown): value is number {
|
||||||
|
return typeof value === 'number';
|
||||||
|
}
|
||||||
|
|
|
@ -190,7 +190,6 @@ export const mouseSelect = mixins(
|
||||||
this.$store.commit('resetSelectedNodes');
|
this.$store.commit('resetSelectedNodes');
|
||||||
this.$store.commit('setLastSelectedNode', null);
|
this.$store.commit('setLastSelectedNode', null);
|
||||||
this.$store.commit('setLastSelectedNodeOutputIndex', null);
|
this.$store.commit('setLastSelectedNodeOutputIndex', null);
|
||||||
this.$store.commit('setActiveNode', null);
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.lastSelectedConnection = null;
|
this.lastSelectedConnection = null;
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
@ -4,7 +4,7 @@ import mixins from 'vue-typed-mixins';
|
||||||
|
|
||||||
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
import { deviceSupportHelpers } from '@/components/mixins/deviceSupportHelpers';
|
||||||
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
import { nodeIndex } from '@/components/mixins/nodeIndex';
|
||||||
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE } from '@/constants';
|
import { NODE_NAME_PREFIX, NO_OP_NODE_TYPE, STICKY_NODE_TYPE } from '@/constants';
|
||||||
import * as CanvasHelpers from '@/views/canvasHelpers';
|
import * as CanvasHelpers from '@/views/canvasHelpers';
|
||||||
import { Endpoint } from 'jsplumb';
|
import { Endpoint } from 'jsplumb';
|
||||||
|
|
||||||
|
@ -221,7 +221,15 @@ export const nodeBase = mixins(
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
this.dragging = true;
|
this.dragging = true;
|
||||||
|
|
||||||
if (params.e && !this.$store.getters.isNodeSelected(this.data.name)) {
|
const isSelected = this.$store.getters.isNodeSelected(this.data.name);
|
||||||
|
const nodeName = this.data.name;
|
||||||
|
if (this.data.type === STICKY_NODE_TYPE && !isSelected) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.$emit('nodeSelected', nodeName, false, true);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.e && !isSelected) {
|
||||||
// Only the node which gets dragged directly gets an event, for all others it is
|
// Only the node which gets dragged directly gets an event, for all others it is
|
||||||
// undefined. So check if the currently dragged node is selected and if not clear
|
// undefined. So check if the currently dragged node is selected and if not clear
|
||||||
// the drag-selection.
|
// the drag-selection.
|
||||||
|
|
|
@ -65,6 +65,7 @@ export const JIRA_TRIGGER_NODE_TYPE = 'n8n-nodes-base.jiraTrigger';
|
||||||
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
export const MICROSOFT_EXCEL_NODE_TYPE = 'n8n-nodes-base.microsoftExcel';
|
||||||
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
export const MICROSOFT_TEAMS_NODE_TYPE = 'n8n-nodes-base.microsoftTeams';
|
||||||
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
export const NO_OP_NODE_TYPE = 'n8n-nodes-base.noOp';
|
||||||
|
export const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
||||||
export const NOTION_TRIGGER_NODE_TYPE = 'n8n-nodes-base.notionTrigger';
|
export const NOTION_TRIGGER_NODE_TYPE = 'n8n-nodes-base.notionTrigger';
|
||||||
export const PAGERDUTY_NODE_TYPE = 'n8n-nodes-base.pagerDuty';
|
export const PAGERDUTY_NODE_TYPE = 'n8n-nodes-base.pagerDuty';
|
||||||
export const SALESFORCE_NODE_TYPE = 'n8n-nodes-base.salesforce';
|
export const SALESFORCE_NODE_TYPE = 'n8n-nodes-base.salesforce';
|
||||||
|
|
|
@ -65,6 +65,7 @@ import {
|
||||||
N8nRadioButtons,
|
N8nRadioButtons,
|
||||||
N8nSelect,
|
N8nSelect,
|
||||||
N8nSpinner,
|
N8nSpinner,
|
||||||
|
N8nSticky,
|
||||||
N8nTabs,
|
N8nTabs,
|
||||||
N8nFormInputs,
|
N8nFormInputs,
|
||||||
N8nFormBox,
|
N8nFormBox,
|
||||||
|
@ -100,6 +101,7 @@ Vue.use(N8nMenuItem);
|
||||||
Vue.use(N8nOption);
|
Vue.use(N8nOption);
|
||||||
Vue.use(N8nSelect);
|
Vue.use(N8nSelect);
|
||||||
Vue.use(N8nSpinner);
|
Vue.use(N8nSpinner);
|
||||||
|
Vue.component('n8n-sticky', N8nSticky);
|
||||||
Vue.use(N8nRadioButtons);
|
Vue.use(N8nRadioButtons);
|
||||||
Vue.component('n8n-square-button', N8nSquareButton);
|
Vue.component('n8n-square-button', N8nSquareButton);
|
||||||
Vue.use(N8nTags);
|
Vue.use(N8nTags);
|
||||||
|
|
|
@ -411,6 +411,7 @@
|
||||||
"nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)",
|
"nodeSettings.waitBetweenTries.description": "How long to wait between each attempt (in milliseconds)",
|
||||||
"nodeSettings.waitBetweenTries.displayName": "Wait Between Tries (ms)",
|
"nodeSettings.waitBetweenTries.displayName": "Wait Between Tries (ms)",
|
||||||
"nodeView.addNode": "Add node",
|
"nodeView.addNode": "Add node",
|
||||||
|
"nodeView.addSticky": "Click to add sticky note",
|
||||||
"nodeView.confirmMessage.beforeRouteLeave.cancelButtonText": "Leave without saving",
|
"nodeView.confirmMessage.beforeRouteLeave.cancelButtonText": "Leave without saving",
|
||||||
"nodeView.confirmMessage.beforeRouteLeave.confirmButtonText": "Save",
|
"nodeView.confirmMessage.beforeRouteLeave.confirmButtonText": "Save",
|
||||||
"nodeView.confirmMessage.beforeRouteLeave.headline": "Save changes before leaving?",
|
"nodeView.confirmMessage.beforeRouteLeave.headline": "Save changes before leaving?",
|
||||||
|
|
|
@ -93,7 +93,11 @@ import {
|
||||||
faUserCircle,
|
faUserCircle,
|
||||||
faUserFriends,
|
faUserFriends,
|
||||||
faUsers,
|
faUsers,
|
||||||
|
faStickyNote as faSolidStickyNote,
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import {
|
||||||
|
faStickyNote,
|
||||||
|
} from '@fortawesome/free-regular-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
||||||
|
|
||||||
function addIcon(icon: any) { // tslint:disable-line:no-any
|
function addIcon(icon: any) { // tslint:disable-line:no-any
|
||||||
|
@ -177,6 +181,8 @@ addIcon(faServer);
|
||||||
addIcon(faSignInAlt);
|
addIcon(faSignInAlt);
|
||||||
addIcon(faSlidersH);
|
addIcon(faSlidersH);
|
||||||
addIcon(faSpinner);
|
addIcon(faSpinner);
|
||||||
|
addIcon(faSolidStickyNote);
|
||||||
|
addIcon(faStickyNote);
|
||||||
addIcon(faStop);
|
addIcon(faStop);
|
||||||
addIcon(faSun);
|
addIcon(faSun);
|
||||||
addIcon(faSync);
|
addIcon(faSync);
|
||||||
|
|
|
@ -169,6 +169,9 @@ class Telemetry {
|
||||||
case 'nodeView.addNodeButton':
|
case 'nodeView.addNodeButton':
|
||||||
this.telemetry.track('User added node to workflow canvas', properties);
|
this.telemetry.track('User added node to workflow canvas', properties);
|
||||||
break;
|
break;
|
||||||
|
case 'nodeView.addSticky':
|
||||||
|
this.telemetry.track('User inserted workflow note', properties);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,8 +21,9 @@
|
||||||
class="node-view"
|
class="node-view"
|
||||||
:style="workflowStyle"
|
:style="workflowStyle"
|
||||||
>
|
>
|
||||||
|
<div v-for="nodeData in nodes" :key="getNodeIndex(nodeData.name)">
|
||||||
<node
|
<node
|
||||||
v-for="nodeData in nodes"
|
v-if="nodeData.type !== STICKY_NODE_TYPE"
|
||||||
@duplicateNode="duplicateNode"
|
@duplicateNode="duplicateNode"
|
||||||
@deselectAllNodes="deselectAllNodes"
|
@deselectAllNodes="deselectAllNodes"
|
||||||
@deselectNode="nodeDeselectedByName"
|
@deselectNode="nodeDeselectedByName"
|
||||||
|
@ -39,11 +40,35 @@
|
||||||
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
||||||
:hideActions="pullConnActive"
|
:hideActions="pullConnActive"
|
||||||
/>
|
/>
|
||||||
|
<Sticky
|
||||||
|
v-else
|
||||||
|
@deselectAllNodes="deselectAllNodes"
|
||||||
|
@deselectNode="nodeDeselectedByName"
|
||||||
|
@nodeSelected="nodeSelectedByName"
|
||||||
|
@removeNode="removeNode"
|
||||||
|
:id="'node-' + getNodeIndex(nodeData.name)"
|
||||||
|
:name="nodeData.name"
|
||||||
|
:isReadOnly="isReadOnly"
|
||||||
|
:instance="instance"
|
||||||
|
:isActive="!!activeNode && activeNode.name === nodeData.name"
|
||||||
|
:nodeViewScale="nodeViewScale"
|
||||||
|
:gridSize="GRID_SIZE"
|
||||||
|
:hideActions="pullConnActive"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DataDisplay :renaming="renamingActive" @valueChanged="valueChanged"/>
|
<DataDisplay :renaming="renamingActive" @valueChanged="valueChanged"/>
|
||||||
<div v-if="!createNodeActive && !isReadOnly" class="node-creator-button" :title="$locale.baseText('nodeView.addNode')" @click="() => openNodeCreator('add_node_button')">
|
<div
|
||||||
<n8n-icon-button size="xlarge" icon="plus" />
|
class="node-buttons-wrapper"
|
||||||
|
v-if="!createNodeActive && !isReadOnly"
|
||||||
|
>
|
||||||
|
<div class="node-creator-button">
|
||||||
|
<n8n-icon-button size="xlarge" icon="plus" @click="() => openNodeCreator('add_node_button')" :title="$locale.baseText('nodeView.addNode')"/>
|
||||||
|
<div class="add-sticky-button" @click="nodeTypeSelected(STICKY_NODE_TYPE)">
|
||||||
|
<n8n-icon-button size="large" :icon="['far', 'note-sticky']" type="outline" :title="$locale.baseText('nodeView.addSticky')"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<node-creator
|
<node-creator
|
||||||
:active="createNodeActive"
|
:active="createNodeActive"
|
||||||
|
@ -122,7 +147,7 @@ import {
|
||||||
} from 'jsplumb';
|
} from 'jsplumb';
|
||||||
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
import { MessageBoxInputData } from 'element-ui/types/message-box';
|
||||||
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
import { jsPlumb, OnConnectionBindInfo } from 'jsplumb';
|
||||||
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
import { MODAL_CANCEL, MODAL_CLOSE, MODAL_CONFIRMED, NODE_NAME_PREFIX, NODE_OUTPUT_DEFAULT_KEY, PLACEHOLDER_EMPTY_WORKFLOW_ID, START_NODE_TYPE, STICKY_NODE_TYPE, VIEWS, WEBHOOK_NODE_TYPE, WORKFLOW_OPEN_MODAL_KEY } from '@/constants';
|
||||||
import { copyPaste } from '@/components/mixins/copyPaste';
|
import { copyPaste } from '@/components/mixins/copyPaste';
|
||||||
import { externalHooks } from '@/components/mixins/externalHooks';
|
import { externalHooks } from '@/components/mixins/externalHooks';
|
||||||
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
import { genericHelpers } from '@/components/mixins/genericHelpers';
|
||||||
|
@ -141,6 +166,7 @@ import Node from '@/components/Node.vue';
|
||||||
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
|
import NodeCreator from '@/components/NodeCreator/NodeCreator.vue';
|
||||||
import NodeSettings from '@/components/NodeSettings.vue';
|
import NodeSettings from '@/components/NodeSettings.vue';
|
||||||
import RunData from '@/components/RunData.vue';
|
import RunData from '@/components/RunData.vue';
|
||||||
|
import Sticky from '@/components/Sticky.vue';
|
||||||
|
|
||||||
import * as CanvasHelpers from './canvasHelpers';
|
import * as CanvasHelpers from './canvasHelpers';
|
||||||
|
|
||||||
|
@ -211,6 +237,7 @@ export default mixins(
|
||||||
NodeCreator,
|
NodeCreator,
|
||||||
NodeSettings,
|
NodeSettings,
|
||||||
RunData,
|
RunData,
|
||||||
|
Sticky,
|
||||||
},
|
},
|
||||||
errorCaptured: (err, vm, info) => {
|
errorCaptured: (err, vm, info) => {
|
||||||
console.error('errorCaptured'); // eslint-disable-line no-console
|
console.error('errorCaptured'); // eslint-disable-line no-console
|
||||||
|
@ -344,6 +371,8 @@ export default mixins(
|
||||||
},
|
},
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
|
GRID_SIZE: CanvasHelpers.GRID_SIZE,
|
||||||
|
STICKY_NODE_TYPE,
|
||||||
createNodeActive: false,
|
createNodeActive: false,
|
||||||
instance: jsPlumb.getInstance(),
|
instance: jsPlumb.getInstance(),
|
||||||
lastSelectedConnection: null as null | Connection,
|
lastSelectedConnection: null as null | Connection,
|
||||||
|
@ -710,7 +739,7 @@ export default mixins(
|
||||||
this.ctrlKeyPressed = true;
|
this.ctrlKeyPressed = true;
|
||||||
} else if (e.key === 'F2' && !this.isReadOnly) {
|
} else if (e.key === 'F2' && !this.isReadOnly) {
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
if (lastSelectedNode !== null) {
|
if (lastSelectedNode !== null && lastSelectedNode.type !== STICKY_NODE_TYPE) {
|
||||||
this.callDebounced('renameNodePrompt', { debounceTime: 1500 }, lastSelectedNode.name);
|
this.callDebounced('renameNodePrompt', { debounceTime: 1500 }, lastSelectedNode.name);
|
||||||
}
|
}
|
||||||
} else if ((e.key === '=' || e.key === '+') && !this.isCtrlKeyPressed(e)) {
|
} else if ((e.key === '=' || e.key === '+') && !this.isCtrlKeyPressed(e)) {
|
||||||
|
@ -777,6 +806,9 @@ export default mixins(
|
||||||
const lastSelectedNode = this.lastSelectedNode;
|
const lastSelectedNode = this.lastSelectedNode;
|
||||||
|
|
||||||
if (lastSelectedNode !== null) {
|
if (lastSelectedNode !== null) {
|
||||||
|
if (lastSelectedNode.type === STICKY_NODE_TYPE && this.isReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$store.commit('setActiveNode', lastSelectedNode.name);
|
this.$store.commit('setActiveNode', lastSelectedNode.name);
|
||||||
}
|
}
|
||||||
} else if (e.key === 'ArrowRight' && e.shiftKey === true) {
|
} else if (e.key === 'ArrowRight' && e.shiftKey === true) {
|
||||||
|
@ -1391,18 +1423,22 @@ export default mixins(
|
||||||
|
|
||||||
this.$store.commit('setStateDirty', true);
|
this.$store.commit('setStateDirty', true);
|
||||||
|
|
||||||
|
if (nodeTypeName === STICKY_NODE_TYPE) {
|
||||||
|
this.$telemetry.trackNodesPanel('nodeView.addSticky', { workflow_id: this.$store.getters.workflowId });
|
||||||
|
} else {
|
||||||
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
this.$externalHooks().run('nodeView.addNodeButton', { nodeTypeName });
|
||||||
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', {
|
this.$telemetry.trackNodesPanel('nodeView.addNodeButton', {
|
||||||
node_type: nodeTypeName,
|
node_type: nodeTypeName,
|
||||||
workflow_id: this.$store.getters.workflowId,
|
workflow_id: this.$store.getters.workflowId,
|
||||||
drag_and_drop: options.dragAndDrop,
|
drag_and_drop: options.dragAndDrop,
|
||||||
} as IDataObject);
|
} as IDataObject);
|
||||||
|
}
|
||||||
|
|
||||||
// Automatically deselect all nodes and select the current one and also active
|
// Automatically deselect all nodes and select the current one and also active
|
||||||
// current node
|
// current node
|
||||||
this.deselectAllNodes();
|
this.deselectAllNodes();
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
this.nodeSelectedByName(newNodeData.name, true);
|
this.nodeSelectedByName(newNodeData.name, nodeTypeName !== STICKY_NODE_TYPE);
|
||||||
});
|
});
|
||||||
|
|
||||||
return newNodeData;
|
return newNodeData;
|
||||||
|
@ -2174,8 +2210,12 @@ export default mixins(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(node.type === STICKY_NODE_TYPE) {
|
||||||
|
this.$telemetry.track('User deleted workflow note', { workflow_id: this.$store.getters.workflowId });
|
||||||
|
} else {
|
||||||
this.$externalHooks().run('node.deleteNode', { node });
|
this.$externalHooks().run('node.deleteNode', { node });
|
||||||
this.$telemetry.track('User deleted node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
|
this.$telemetry.track('User deleted node', { node_type: node.type, workflow_id: this.$store.getters.workflowId });
|
||||||
|
}
|
||||||
|
|
||||||
let waitForNewConnection = false;
|
let waitForNewConnection = false;
|
||||||
// connect nodes before/after deleted node
|
// connect nodes before/after deleted node
|
||||||
|
@ -2869,6 +2909,28 @@ export default mixins(
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.node-buttons-wrapper {
|
||||||
|
position: fixed;
|
||||||
|
width: 150px;
|
||||||
|
height: 200px;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.add-sticky-button {
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
opacity: 0;
|
||||||
|
transition: .1s;
|
||||||
|
transition-timing-function: linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.add-sticky-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.node-creator-button {
|
.node-creator-button {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getStyleTokenValue } from "@/components/helpers";
|
import { getStyleTokenValue, isNumber } from "@/components/helpers";
|
||||||
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE } from "@/constants";
|
import { NODE_OUTPUT_DEFAULT_KEY, START_NODE_TYPE, STICKY_NODE_TYPE } from "@/constants";
|
||||||
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
import { IBounds, INodeUi, IZoomConfig, XYPosition } from "@/Interface";
|
||||||
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
import { Connection, Endpoint, Overlay, OverlaySpec, PaintStyle } from "jsplumb";
|
||||||
import {
|
import {
|
||||||
|
@ -217,17 +217,22 @@ export const getLeftmostTopNode = (nodes: INodeUi[]): INodeUi => {
|
||||||
|
|
||||||
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
export const getWorkflowCorners = (nodes: INodeUi[]): IBounds => {
|
||||||
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
return nodes.reduce((accu: IBounds, node: INodeUi) => {
|
||||||
if (node.position[0] < accu.minX) {
|
const xOffset = node.type === STICKY_NODE_TYPE && isNumber(node.parameters.width) ? node.parameters.width : NODE_SIZE;
|
||||||
accu.minX = node.position[0];
|
const yOffset = node.type === STICKY_NODE_TYPE && isNumber(node.parameters.height) ? node.parameters.height : NODE_SIZE;
|
||||||
|
const x = node.position[0];
|
||||||
|
const y = node.position[1];
|
||||||
|
|
||||||
|
if (x < accu.minX) {
|
||||||
|
accu.minX = x;
|
||||||
}
|
}
|
||||||
if (node.position[1] < accu.minY) {
|
if (y < accu.minY) {
|
||||||
accu.minY = node.position[1];
|
accu.minY = y;
|
||||||
}
|
}
|
||||||
if (node.position[0] > accu.maxX) {
|
if ((x + xOffset) > accu.maxX) {
|
||||||
accu.maxX = node.position[0];
|
accu.maxX = x + xOffset;
|
||||||
}
|
}
|
||||||
if (node.position[1] > accu.maxY) {
|
if ((y + yOffset) > accu.maxY) {
|
||||||
accu.maxY = node.position[1];
|
accu.maxY = y + yOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accu;
|
return accu;
|
||||||
|
@ -592,6 +597,7 @@ export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {off
|
||||||
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
const {minX, minY, maxX, maxY} = getWorkflowCorners(nodes);
|
||||||
const sidebarWidth = addComponentPadding? SIDEBAR_WIDTH: 0;
|
const sidebarWidth = addComponentPadding? SIDEBAR_WIDTH: 0;
|
||||||
const headerHeight = addComponentPadding? HEADER_HEIGHT: 0;
|
const headerHeight = addComponentPadding? HEADER_HEIGHT: 0;
|
||||||
|
const footerHeight = addComponentPadding? 200: 100;
|
||||||
|
|
||||||
const PADDING = NODE_SIZE * 4;
|
const PADDING = NODE_SIZE * 4;
|
||||||
|
|
||||||
|
@ -605,10 +611,10 @@ export const getZoomToFit = (nodes: INodeUi[], addComponentPadding = true): {off
|
||||||
|
|
||||||
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
const zoomLevel = Math.min(scaleX, scaleY, 1);
|
||||||
let xOffset = (minX * -1) * zoomLevel + sidebarWidth; // find top right corner
|
let xOffset = (minX * -1) * zoomLevel + sidebarWidth; // find top right corner
|
||||||
xOffset += (editorWidth - sidebarWidth - (maxX - minX + NODE_SIZE) * zoomLevel) / 2; // add padding to center workflow
|
xOffset += (editorWidth - sidebarWidth - (maxX - minX) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
let yOffset = (minY * -1) * zoomLevel + headerHeight; // find top right corner
|
let yOffset = (minY * -1) * zoomLevel + headerHeight; // find top right corner
|
||||||
yOffset += (editorHeight - headerHeight - (maxY - minY + NODE_SIZE * 2) * zoomLevel) / 2; // add padding to center workflow
|
yOffset += (editorHeight - headerHeight - (maxY - minY + footerHeight) * zoomLevel) / 2; // add padding to center workflow
|
||||||
|
|
||||||
return {
|
return {
|
||||||
zoomLevel,
|
zoomLevel,
|
||||||
|
|
18
packages/nodes-base/nodes/StickyNote/StickyNote.node.json
Normal file
18
packages/nodes-base/nodes/StickyNote/StickyNote.node.json
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.stickyNote",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": [
|
||||||
|
"Core Nodes"
|
||||||
|
],
|
||||||
|
"alias": [
|
||||||
|
"Comments",
|
||||||
|
"Notes",
|
||||||
|
"Sticky"
|
||||||
|
],
|
||||||
|
"subcategories": {
|
||||||
|
"Core Nodes": [
|
||||||
|
"Helpers"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
52
packages/nodes-base/nodes/StickyNote/StickyNote.node.ts
Normal file
52
packages/nodes-base/nodes/StickyNote/StickyNote.node.ts
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
import { IExecuteFunctions } from 'n8n-core';
|
||||||
|
import {
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
|
||||||
|
export class StickyNote implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Sticky Note',
|
||||||
|
name: 'stickyNote',
|
||||||
|
icon: 'fa:sticky-note',
|
||||||
|
group: ['input'],
|
||||||
|
version: 1,
|
||||||
|
description: 'Make your workflow easier to understand',
|
||||||
|
defaults: {
|
||||||
|
name: 'Note',
|
||||||
|
color: '#FFD233',
|
||||||
|
},
|
||||||
|
inputs: [],
|
||||||
|
outputs: [],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Content',
|
||||||
|
name: 'content',
|
||||||
|
type: 'string',
|
||||||
|
required: true,
|
||||||
|
default: `## I'm a note \n**Double click** to edit me. [Guide](https://docs.n8n.io/workflows/sticky-notes/)`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'height',
|
||||||
|
name: 'height',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
default: 160,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'width',
|
||||||
|
name: 'width',
|
||||||
|
type: 'number',
|
||||||
|
required: true,
|
||||||
|
default: 240,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const items = this.getInputData();
|
||||||
|
return this.prepareOutputData(items);
|
||||||
|
}
|
||||||
|
}
|
|
@ -551,6 +551,7 @@
|
||||||
"dist/nodes/Netlify/NetlifyTrigger.node.js",
|
"dist/nodes/Netlify/NetlifyTrigger.node.js",
|
||||||
"dist/nodes/NextCloud/NextCloud.node.js",
|
"dist/nodes/NextCloud/NextCloud.node.js",
|
||||||
"dist/nodes/NocoDB/NocoDB.node.js",
|
"dist/nodes/NocoDB/NocoDB.node.js",
|
||||||
|
"dist/nodes/StickyNote/StickyNote.node.js",
|
||||||
"dist/nodes/NoOp/NoOp.node.js",
|
"dist/nodes/NoOp/NoOp.node.js",
|
||||||
"dist/nodes/Onfleet/Onfleet.node.js",
|
"dist/nodes/Onfleet/Onfleet.node.js",
|
||||||
"dist/nodes/Onfleet/OnfleetTrigger.node.js",
|
"dist/nodes/Onfleet/OnfleetTrigger.node.js",
|
||||||
|
|
|
@ -1410,12 +1410,24 @@ export interface INodesGraph {
|
||||||
node_types: string[];
|
node_types: string[];
|
||||||
node_connections: IDataObject[];
|
node_connections: IDataObject[];
|
||||||
nodes: INodesGraphNode;
|
nodes: INodesGraphNode;
|
||||||
|
notes: INotesGraphNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface INodesGraphNode {
|
export interface INodesGraphNode {
|
||||||
[key: string]: INodeGraphItem;
|
[key: string]: INodeGraphItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface INotesGraphNode {
|
||||||
|
[key: string]: INoteGraphItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface INoteGraphItem {
|
||||||
|
overlapping: boolean;
|
||||||
|
position: [number, number];
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface INodeGraphItem {
|
export interface INodeGraphItem {
|
||||||
type: string;
|
type: string;
|
||||||
resource?: string;
|
resource?: string;
|
||||||
|
|
|
@ -9,13 +9,57 @@ import {
|
||||||
IWorkflowBase,
|
IWorkflowBase,
|
||||||
INodeTypes,
|
INodeTypes,
|
||||||
} from '.';
|
} from '.';
|
||||||
|
import { INodeType } from './Interfaces';
|
||||||
|
|
||||||
import { getInstance as getLoggerInstance } from './LoggerProxy';
|
import { getInstance as getLoggerInstance } from './LoggerProxy';
|
||||||
|
|
||||||
|
const STICKY_NODE_TYPE = 'n8n-nodes-base.stickyNote';
|
||||||
|
|
||||||
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
|
export function getNodeTypeForName(workflow: IWorkflowBase, nodeName: string): INode | undefined {
|
||||||
return workflow.nodes.find((node) => node.name === nodeName);
|
return workflow.nodes.find((node) => node.name === nodeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isNumber(value: unknown): value is number {
|
||||||
|
return typeof value === 'number';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStickyDimensions(note: INode, stickyType: INodeType | undefined) {
|
||||||
|
const heightProperty = stickyType?.description.properties.find(
|
||||||
|
(property) => property.name === 'height',
|
||||||
|
);
|
||||||
|
const widthProperty = stickyType?.description.properties.find(
|
||||||
|
(property) => property.name === 'width',
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultHeight =
|
||||||
|
heightProperty && isNumber(heightProperty?.default) ? heightProperty.default : 0;
|
||||||
|
const defaultWidth =
|
||||||
|
widthProperty && isNumber(widthProperty?.default) ? widthProperty.default : 0;
|
||||||
|
|
||||||
|
const height: number = isNumber(note.parameters.height) ? note.parameters.height : defaultHeight;
|
||||||
|
const width: number = isNumber(note.parameters.width) ? note.parameters.width : defaultWidth;
|
||||||
|
|
||||||
|
return {
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type XYPosition = [number, number];
|
||||||
|
|
||||||
|
function areOverlapping(
|
||||||
|
topLeft: XYPosition,
|
||||||
|
bottomRight: XYPosition,
|
||||||
|
targetPos: XYPosition,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
targetPos[0] > topLeft[0] &&
|
||||||
|
targetPos[1] > topLeft[1] &&
|
||||||
|
targetPos[0] < bottomRight[0] &&
|
||||||
|
targetPos[1] < bottomRight[1]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function generateNodesGraph(
|
export function generateNodesGraph(
|
||||||
workflow: IWorkflowBase,
|
workflow: IWorkflowBase,
|
||||||
nodeTypes: INodeTypes,
|
nodeTypes: INodeTypes,
|
||||||
|
@ -24,11 +68,32 @@ export function generateNodesGraph(
|
||||||
node_types: [],
|
node_types: [],
|
||||||
node_connections: [],
|
node_connections: [],
|
||||||
nodes: {},
|
nodes: {},
|
||||||
|
notes: {},
|
||||||
};
|
};
|
||||||
const nodeNameAndIndex: INodeNameIndex = {};
|
const nodeNameAndIndex: INodeNameIndex = {};
|
||||||
|
|
||||||
try {
|
try {
|
||||||
workflow.nodes.forEach((node: INode, index: number) => {
|
const notes = workflow.nodes.filter((node) => node.type === STICKY_NODE_TYPE);
|
||||||
|
const otherNodes = workflow.nodes.filter((node) => node.type !== STICKY_NODE_TYPE);
|
||||||
|
|
||||||
|
notes.forEach((stickyNote: INode, index: number) => {
|
||||||
|
const stickyType = nodeTypes.getByNameAndVersion(STICKY_NODE_TYPE, stickyNote.typeVersion);
|
||||||
|
const { height, width } = getStickyDimensions(stickyNote, stickyType);
|
||||||
|
|
||||||
|
const topLeft = stickyNote.position;
|
||||||
|
const bottomRight: [number, number] = [topLeft[0] + width, topLeft[1] + height];
|
||||||
|
const overlapping = Boolean(
|
||||||
|
otherNodes.find((node) => areOverlapping(topLeft, bottomRight, node.position)),
|
||||||
|
);
|
||||||
|
nodesGraph.notes[index] = {
|
||||||
|
overlapping,
|
||||||
|
position: topLeft,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
otherNodes.forEach((node: INode, index: number) => {
|
||||||
nodesGraph.node_types.push(node.type);
|
nodesGraph.node_types.push(node.type);
|
||||||
const nodeItem: INodeGraphItem = {
|
const nodeItem: INodeGraphItem = {
|
||||||
type: node.type,
|
type: node.type,
|
||||||
|
|
Loading…
Reference in a new issue