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:
Mutasem Aldmour
2022-04-25 12:38:37 +02:00
committed by GitHub
parent d446f9e281
commit 31dd01f9cb
34 changed files with 9501 additions and 67038 deletions

View File

@@ -79,6 +79,7 @@
"vue-loader": "^15.9.7",
"vue-property-decorator": "^9.1.2",
"vue-template-compiler": "^2.6.11",
"vue-typed-mixins": "^0.2.0",
"vue2-boring-avatars": "0.3.4",
"xss": "^1.0.10"
}

View File

@@ -69,7 +69,6 @@ export default {
default: false,
},
icon: {
type: String,
},
round: {
type: Boolean,

View File

@@ -26,7 +26,6 @@ export default {
},
props: {
icon: {
type: String,
required: true,
},
size: {

View File

@@ -40,7 +40,6 @@ export default {
default: false,
},
icon: {
type: String,
required: true,
},
theme: {

View File

@@ -1,6 +1,10 @@
<template>
<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-for="(block, index) in loadingBlocks"
:key="index">
@@ -59,6 +63,9 @@ export default {
content: {
type: String,
},
withMultiBreaks: {
type: Boolean,
},
images: {
type: Array,
},
@@ -75,6 +82,10 @@ export default {
return 3;
},
},
theme: {
type: String,
default: 'markdown',
},
options: {
type: Object,
default() {
@@ -106,7 +117,11 @@ export default {
}
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&nbsp;\n');
}
const html = this.md.render(escapeMarkdown(contentToRender));
const safeHtml = xss(html, {
onTagAttr: (tag, name, value, isWhiteAttr) => {
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 {
margin: var(--spacing-2xl);
}

View 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>

View File

@@ -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,
};

View 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>

View File

@@ -0,0 +1,3 @@
import Sticky from './Sticky.vue';
export default Sticky;

View File

@@ -57,6 +57,7 @@ import N8nOption from './N8nOption';
import N8nRadioButtons from './N8nRadioButtons';
import N8nSelect from './N8nSelect';
import N8nSpinner from './N8nSpinner';
import N8nSticky from './N8nSticky';
import N8nSquareButton from './N8nSquareButton';
import N8nTags from './N8nTags';
import N8nTabs from './N8nTabs';
@@ -93,6 +94,7 @@ export {
N8nRadioButtons,
N8nSelect,
N8nSpinner,
N8nSticky,
N8nSquareButton,
N8nTabs,
N8nTags,

View File

@@ -16,4 +16,5 @@ export default {
config.minimum > 1 ? 's' : ''
}`),
"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>`,
};

View File

@@ -166,3 +166,17 @@ import ColorCircles from './ColorCircles.vue';
}}
</Story>
</Canvas>
## Sticky
<Canvas>
<Story name="sticky">
{{
template: `<color-circles :colors="['--color-sticky-default-background', '--color-sticky-default-border']" />`,
components: {
ColorCircles,
},
}}
</Story>
</Canvas>

View File

@@ -3,6 +3,7 @@ export const escapeMarkdown = (html: string | undefined): string => {
return '';
}
const escaped = html.replace(/</g, "&lt;").replace(/>/g, "&gt;");
// unescape greater than quotes at start of line
const withQuotes = escaped.replace(/^((\s)*(&gt;)+)+\s*/gm, (matches) => {
return matches.replace(/&gt;/g, '>');

View File

@@ -70,13 +70,6 @@
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-s: 60%;
--color-success-l: 40.4%;
@@ -340,6 +333,24 @@
--color-json-line: #bfcbd9;
--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-large: 8px;
--border-radius-base: 4px;