mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
* feat: Added vite.js dependencies. * chore: Removed tests folder to follow same structure as design-system * chore: Removed unused testing config. * chore: Created vite.js index.html * refactor: Updated scss structure and imports. * refactor: Updated workflow building. * fix: Cleared up all workflow dependency cycles. Added proper package.json imports config. * feat: Got a working build using Vite. Need to fix issues next. * fix: Progress! Getting process.env error. * fix: Changed process.env to import.meta.env. * fix: Fixed circular imports that used require(). Fixed monaco editor. * chore: Removed commented code. * chore: Cleaned up package.json * feat: Made necessary changes to replace base path in css files. * feat: Serve CSS files for `editor-ui` Vite migration (#4069) ⚡ Serve CSS files for Vite migration * chore: Fixed package-lock.json. * fix: Fixed build after centralized tsconfig update. * fix: Removed lodash-es replacement. * fix: Commented out vitest test command. * style: Fixed linting issues. * fix: Added lodash-es hotfix back. * chore: Updated package-lock.json * refactor: Renamed all n8n scss variables to no longer be defined as private. * feat(editor): add application-wide el-button replacement. * fix(editor): Fix import in page alert after merge. * chore(editor): update package-lock.json. * fix: Case sensitive lodash-es replacement for vue-agile. * fix: add alias for lodash-es camelcase import. * fix: add patch-package support for fixing quill * feat: add patch-package on postinstall * fix: update quill patch path. * refactor: rename quill patch * fix: update quill version. * fix: update quill patch * fix: fix linting rules after installing eslint in design-system * fix: update date picker button to have primary color * test: update callout component snapshots * fix(editor): fix linting issues in editor after enabling eslint * fix(cli): add /assets/* to auth ignore endpoints in server * chore: update package-lock.json * chore: update package-lock.json * fix(editor): fix linting issues * feat: add vite-legacy support * fix: update workflow package interface imports to type imports. * chore: update package-lock.json * fix(editor) fix importing translations other than english * fix(editor): remove test command until vitest is added * fix: increase memory allocation for vite build * fix: add patch-package patches to n8n-custom docker build * fix: add performance and load time improvements * fix: add proper typing to setNodeType * chore: update package-lock.json * style: use generic type for reduce in setNodeType Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
330 lines
6.6 KiB
Vue
330 lines
6.6 KiB
Vue
<template>
|
|
<div class="n8n-markdown">
|
|
<div
|
|
v-if="!loading"
|
|
ref="editor"
|
|
class="ph-no-capture"
|
|
:class="$style[theme]" v-html="htmlContent"
|
|
@click="onClick"
|
|
/>
|
|
<div v-else :class="$style.markdown">
|
|
<div v-for="(block, index) in loadingBlocks"
|
|
:key="index">
|
|
<n8n-loading
|
|
:loading="loading"
|
|
:rows="loadingRows"
|
|
animated
|
|
variant="p"
|
|
/>
|
|
<div :class="$style.spacer" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import N8nLoading from '../N8nLoading';
|
|
import Markdown from 'markdown-it';
|
|
|
|
// @ts-ignore
|
|
import markdownLink from 'markdown-it-link-attributes';
|
|
// @ts-ignore
|
|
import markdownEmoji from 'markdown-it-emoji';
|
|
// @ts-ignore
|
|
import markdownTasklists from 'markdown-it-task-lists';
|
|
|
|
import xss, { friendlyAttrValue } from 'xss';
|
|
import { escapeMarkdown } from '../../utils/markdown';
|
|
|
|
const DEFAULT_OPTIONS_MARKDOWN = {
|
|
html: true,
|
|
linkify: true,
|
|
typographer: true,
|
|
breaks: true,
|
|
};
|
|
|
|
const DEFAULT_OPTIONS_LINK_ATTRIBUTES = {
|
|
attrs: {
|
|
target: '_blank',
|
|
rel: 'noopener',
|
|
},
|
|
};
|
|
|
|
const DEFAULT_OPTIONS_TASKLISTS = {
|
|
label: true,
|
|
labelAfter: true,
|
|
};
|
|
|
|
interface IImage {
|
|
id: string;
|
|
url: string;
|
|
}
|
|
|
|
import Vue from 'vue';
|
|
|
|
export default Vue.extend({
|
|
components: {
|
|
N8nLoading,
|
|
},
|
|
name: 'n8n-markdown',
|
|
props: {
|
|
content: {
|
|
type: String,
|
|
},
|
|
withMultiBreaks: {
|
|
type: Boolean,
|
|
},
|
|
images: {
|
|
type: Array,
|
|
},
|
|
loading: {
|
|
type: Boolean,
|
|
},
|
|
loadingBlocks: {
|
|
type: Number,
|
|
default: 2,
|
|
},
|
|
loadingRows: {
|
|
type: Number,
|
|
default: () => {
|
|
return 3;
|
|
},
|
|
},
|
|
theme: {
|
|
type: String,
|
|
default: 'markdown',
|
|
},
|
|
options: {
|
|
type: Object,
|
|
default() {
|
|
return {
|
|
markdown: DEFAULT_OPTIONS_MARKDOWN,
|
|
linkAttributes: DEFAULT_OPTIONS_LINK_ATTRIBUTES,
|
|
tasklists: DEFAULT_OPTIONS_TASKLISTS,
|
|
};
|
|
},
|
|
},
|
|
},
|
|
computed: {
|
|
htmlContent(): string {
|
|
if (!this.content) {
|
|
return '';
|
|
}
|
|
|
|
const imageUrls: { [key: string]: string } = {};
|
|
if (this.images) {
|
|
// @ts-ignore
|
|
this.images.forEach((image: IImage) => {
|
|
if (!image) {
|
|
// Happens if an image got deleted but the workflow
|
|
// still has a reference to it
|
|
return;
|
|
}
|
|
imageUrls[image.id] = image.url;
|
|
});
|
|
}
|
|
|
|
const fileIdRegex = new RegExp('fileId:([0-9]+)');
|
|
const imageFilesRegex = /\.(jpeg|jpg|gif|png|webp|bmp|tif|tiff|apng|svg|avif)$/;
|
|
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, {
|
|
onTagAttr: (tag, name, value, isWhiteAttr) => {
|
|
if (tag === 'img' && name === 'src') {
|
|
if (value.match(fileIdRegex)) {
|
|
const id = value.split('fileId:')[1];
|
|
return `src=${friendlyAttrValue(imageUrls[id])}` || '';
|
|
}
|
|
// Only allow http requests to supported image files from the `static` directory
|
|
const isImageFile = value.split('#')[0].match(/\.(jpeg|jpg|gif|png|webp)$/) !== null;
|
|
const isStaticImageFile = isImageFile && value.startsWith('/static/');
|
|
if (!value.startsWith('https://') && !isStaticImageFile) {
|
|
return '';
|
|
}
|
|
}
|
|
// Return nothing, means keep the default handling measure
|
|
},
|
|
onTag (tag, code, options) {
|
|
if (tag === 'img' && code.includes(`alt="workflow-screenshot"`)) {
|
|
return '';
|
|
}
|
|
// return nothing, keep tag
|
|
},
|
|
});
|
|
|
|
return safeHtml;
|
|
},
|
|
},
|
|
data() {
|
|
return {
|
|
md: new Markdown(this.options.markdown) // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
|
.use(markdownLink, this.options.linkAttributes) // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
|
.use(markdownEmoji)
|
|
.use(markdownTasklists, this.options.tasklists), // eslint-disable-line @typescript-eslint/no-unsafe-member-access
|
|
};
|
|
},
|
|
methods: {
|
|
onClick(event: MouseEvent) {
|
|
let clickedLink = null;
|
|
|
|
if(event.target instanceof HTMLAnchorElement) {
|
|
clickedLink = event.target;
|
|
}
|
|
|
|
if(event.target instanceof HTMLElement && event.target.matches('a *')) {
|
|
const parentLink = event.target.closest('a');
|
|
if(parentLink) {
|
|
clickedLink = parentLink;
|
|
}
|
|
}
|
|
this.$emit('markdown-click', clickedLink, event);
|
|
},
|
|
},
|
|
});
|
|
</script>
|
|
|
|
<style lang="scss" module>
|
|
.markdown {
|
|
color: var(--color-text-base);
|
|
|
|
* {
|
|
font-size: var(--font-size-m);
|
|
line-height: var(--font-line-height-xloose);
|
|
}
|
|
|
|
h1, h2, h3, h4 {
|
|
margin-bottom: var(--spacing-s);
|
|
font-size: var(--font-size-m);
|
|
font-weight: var(--font-weight-bold);
|
|
}
|
|
|
|
h3, h4 {
|
|
font-weight: var(--font-weight-bold);
|
|
}
|
|
|
|
p,
|
|
span {
|
|
margin-bottom: var(--spacing-s);
|
|
}
|
|
|
|
ul, ol {
|
|
margin-bottom: var(--spacing-s);
|
|
padding-left: var(--spacing-m);
|
|
|
|
li {
|
|
margin-top: 0.25em;
|
|
}
|
|
}
|
|
|
|
pre {
|
|
margin-bottom: var(--spacing-s);
|
|
display: grid;
|
|
}
|
|
|
|
pre > code {
|
|
display: block;
|
|
padding: var(--spacing-s);
|
|
color: var(--color-text-dark);
|
|
background-color: var(--color-background-base);
|
|
overflow-x: auto;
|
|
}
|
|
|
|
li > code,
|
|
p > code {
|
|
padding: 0 var(--spacing-4xs);
|
|
color: var(--color-text-dark);
|
|
background-color: var(--color-background-base);
|
|
}
|
|
|
|
.label {
|
|
color: var(--color-text-base);
|
|
}
|
|
|
|
img {
|
|
width: 100%;
|
|
max-height: 90vh;
|
|
object-fit: cover;
|
|
border: var(--border-width-base) var(--color-foreground-base) var(--border-style-base);
|
|
border-radius: var(--border-radius-large);
|
|
}
|
|
|
|
blockquote {
|
|
padding-left: 10px;
|
|
font-style: italic;
|
|
border-left: var(--border-color-base) 2px solid;
|
|
}
|
|
}
|
|
|
|
.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;
|
|
|
|
&[src*="#full-width"] {
|
|
width: 100%;
|
|
}
|
|
}
|
|
}
|
|
|
|
.spacer {
|
|
margin: var(--spacing-2xl);
|
|
}
|
|
</style>
|