mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
fix(editor): Replace v-html with custom directive to sanitize html (#10804)
Co-authored-by: Tomi Turtiainen <10324676+tomi@users.noreply.github.com>
This commit is contained in:
parent
989f69d1f4
commit
44e5fb9b06
|
@ -29,7 +29,6 @@
|
||||||
"@types/sanitize-html": "^2.11.0",
|
"@types/sanitize-html": "^2.11.0",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vitest/coverage-v8": "catalog:frontend",
|
"@vitest/coverage-v8": "catalog:frontend",
|
||||||
"@vue/test-utils": "^2.4.3",
|
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"sass": "^1.64.1",
|
"sass": "^1.64.1",
|
||||||
|
|
|
@ -151,8 +151,7 @@ function growInput() {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.blockBody">
|
<div :class="$style.blockBody">
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<span v-n8n-html="renderMarkdown(message.content)"></span>
|
||||||
<span v-html="renderMarkdown(message.content)"></span>
|
|
||||||
<BlinkingCursor
|
<BlinkingCursor
|
||||||
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
|
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
|
||||||
/>
|
/>
|
||||||
|
@ -160,19 +159,20 @@ function growInput() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="message.type === 'text'" :class="$style.textMessage">
|
<div v-else-if="message.type === 'text'" :class="$style.textMessage">
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
<span
|
||||||
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
|
v-if="message.role === 'user'"
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
v-n8n-html="renderMarkdown(message.content)"
|
||||||
|
></span>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
|
v-n8n-html="renderMarkdown(message.content)"
|
||||||
:class="$style.assistantText"
|
:class="$style.assistantText"
|
||||||
v-html="renderMarkdown(message.content)"
|
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-if="message?.codeSnippet"
|
v-if="message?.codeSnippet"
|
||||||
:class="$style['code-snippet']"
|
:class="$style['code-snippet']"
|
||||||
data-test-id="assistant-code-snippet"
|
data-test-id="assistant-code-snippet"
|
||||||
v-html="renderMarkdown(message.codeSnippet).trim()"
|
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
|
||||||
></div>
|
></div>
|
||||||
<BlinkingCursor
|
<BlinkingCursor
|
||||||
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { render } from '@testing-library/vue';
|
import { render } from '@testing-library/vue';
|
||||||
import AskAssistantChat from '../AskAssistantChat.vue';
|
import AskAssistantChat from '../AskAssistantChat.vue';
|
||||||
|
import { n8nHtml } from 'n8n-design-system/directives';
|
||||||
|
|
||||||
describe('AskAssistantChat', () => {
|
describe('AskAssistantChat', () => {
|
||||||
it('renders default placeholder chat correctly', () => {
|
it('renders default placeholder chat correctly', () => {
|
||||||
|
@ -12,6 +13,11 @@ describe('AskAssistantChat', () => {
|
||||||
});
|
});
|
||||||
it('renders chat with messages correctly', () => {
|
it('renders chat with messages correctly', () => {
|
||||||
const { container } = render(AskAssistantChat, {
|
const { container } = render(AskAssistantChat, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { firstName: 'Kobi', lastName: 'Dog' },
|
user: { firstName: 'Kobi', lastName: 'Dog' },
|
||||||
messages: [
|
messages: [
|
||||||
|
@ -86,6 +92,11 @@ describe('AskAssistantChat', () => {
|
||||||
});
|
});
|
||||||
it('renders streaming chat correctly', () => {
|
it('renders streaming chat correctly', () => {
|
||||||
const { container } = render(AskAssistantChat, {
|
const { container } = render(AskAssistantChat, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { firstName: 'Kobi', lastName: 'Dog' },
|
user: { firstName: 'Kobi', lastName: 'Dog' },
|
||||||
messages: [
|
messages: [
|
||||||
|
@ -105,6 +116,11 @@ describe('AskAssistantChat', () => {
|
||||||
});
|
});
|
||||||
it('renders end of session chat correctly', () => {
|
it('renders end of session chat correctly', () => {
|
||||||
const { container } = render(AskAssistantChat, {
|
const { container } = render(AskAssistantChat, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { firstName: 'Kobi', lastName: 'Dog' },
|
user: { firstName: 'Kobi', lastName: 'Dog' },
|
||||||
messages: [
|
messages: [
|
||||||
|
@ -130,6 +146,11 @@ describe('AskAssistantChat', () => {
|
||||||
});
|
});
|
||||||
it('renders message with code snippet', () => {
|
it('renders message with code snippet', () => {
|
||||||
const { container } = render(AskAssistantChat, {
|
const { container } = render(AskAssistantChat, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
user: { firstName: 'Kobi', lastName: 'Dog' },
|
user: { firstName: 'Kobi', lastName: 'Dog' },
|
||||||
messages: [
|
messages: [
|
||||||
|
|
|
@ -129,9 +129,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
|
@ -145,7 +142,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
@ -438,7 +434,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<span>
|
<span>
|
||||||
<p>
|
<p>
|
||||||
Give it to me
|
Give it to me
|
||||||
|
@ -516,7 +511,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="blockBody"
|
class="blockBody"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<span>
|
<span>
|
||||||
<p>
|
<p>
|
||||||
Solution steps:
|
Solution steps:
|
||||||
|
@ -1060,9 +1054,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
|
@ -1076,7 +1067,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
@ -1309,9 +1299,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
|
||||||
<div
|
<div
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
|
@ -1325,7 +1312,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="code-snippet"
|
class="code-snippet"
|
||||||
data-test-id="assistant-code-snippet"
|
data-test-id="assistant-code-snippet"
|
||||||
|
@ -1552,9 +1538,6 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||||
<div
|
<div
|
||||||
class="textMessage"
|
class="textMessage"
|
||||||
>
|
>
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<div
|
<div
|
||||||
class="assistantText"
|
class="assistantText"
|
||||||
>
|
>
|
||||||
|
@ -1568,7 +1551,6 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,7 +37,7 @@ withDefaults(defineProps<ActionBoxProps>(), {
|
||||||
<div :class="$style.description" @click="$emit('descriptionClick', $event)">
|
<div :class="$style.description" @click="$emit('descriptionClick', $event)">
|
||||||
<N8nText color="text-base">
|
<N8nText color="text-base">
|
||||||
<slot name="description">
|
<slot name="description">
|
||||||
<span v-html="description"></span>
|
<span v-n8n-html="description"></span>
|
||||||
</slot>
|
</slot>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,7 +61,7 @@ withDefaults(defineProps<ActionBoxProps>(), {
|
||||||
:class="$style.callout"
|
:class="$style.callout"
|
||||||
>
|
>
|
||||||
<N8nText color="text-base">
|
<N8nText color="text-base">
|
||||||
<span size="small" v-html="calloutText"></span>
|
<span size="small" v-n8n-html="calloutText"></span>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</N8nCallout>
|
</N8nCallout>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -75,7 +75,7 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
|
||||||
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
|
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
|
||||||
<n8n-tooltip :disabled="!item.tooltip">
|
<n8n-tooltip :disabled="!item.tooltip">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div @click="onTooltipClick(item.id, $event)" v-html="item.tooltip"></div>
|
<div @click="onTooltipClick(item.id, $event)" v-n8n-html="item.tooltip"></div>
|
||||||
</template>
|
</template>
|
||||||
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
|
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
|
@ -83,7 +83,7 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<N8nText color="text-base" size="small" align="left">
|
<N8nText color="text-base" size="small" align="left">
|
||||||
<span v-html="description"></span>
|
<span v-n8n-html="description"></span>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
<slot name="customContent"></slot>
|
<slot name="customContent"></slot>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -58,7 +58,7 @@ const addTargetBlank = (html: string) =>
|
||||||
<N8nTooltip placement="top" :popper-class="$style.tooltipPopper" :show-after="300">
|
<N8nTooltip placement="top" :popper-class="$style.tooltipPopper" :show-after="300">
|
||||||
<N8nIcon icon="question-circle" size="small" />
|
<N8nIcon icon="question-circle" size="small" />
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-html="addTargetBlank(tooltipText)" />
|
<div v-n8n-html="addTargetBlank(tooltipText)" />
|
||||||
</template>
|
</template>
|
||||||
</N8nTooltip>
|
</N8nTooltip>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -202,7 +202,7 @@ const onCheckboxChange = (index: number) => {
|
||||||
@click="onClick"
|
@click="onClick"
|
||||||
@mousedown="onMouseDown"
|
@mousedown="onMouseDown"
|
||||||
@change="onChange"
|
@change="onChange"
|
||||||
v-html="htmlContent"
|
v-n8n-html="htmlContent"
|
||||||
/>
|
/>
|
||||||
<div v-else :class="$style.markdown">
|
<div v-else :class="$style.markdown">
|
||||||
<div v-for="(_, index) in loadingBlocks" :key="index">
|
<div v-for="(_, index) in loadingBlocks" :key="index">
|
||||||
|
|
|
@ -1,10 +1,16 @@
|
||||||
import { render, fireEvent } from '@testing-library/vue';
|
import { render, fireEvent } from '@testing-library/vue';
|
||||||
import N8nMarkdown from '../Markdown.vue';
|
import N8nMarkdown from '../Markdown.vue';
|
||||||
|
import { n8nHtml } from 'n8n-design-system/directives';
|
||||||
|
|
||||||
describe('components', () => {
|
describe('components', () => {
|
||||||
describe('N8nMarkdown', () => {
|
describe('N8nMarkdown', () => {
|
||||||
it('should render unchecked checkboxes', () => {
|
it('should render unchecked checkboxes', () => {
|
||||||
const wrapper = render(N8nMarkdown, {
|
const wrapper = render(N8nMarkdown, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
|
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
|
||||||
},
|
},
|
||||||
|
@ -18,6 +24,11 @@ describe('components', () => {
|
||||||
|
|
||||||
it('should render checked checkboxes', () => {
|
it('should render checked checkboxes', () => {
|
||||||
const wrapper = render(N8nMarkdown, {
|
const wrapper = render(N8nMarkdown, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
content: '__TODO__\n- [X] Buy milk\n- [X] Buy socks\n',
|
content: '__TODO__\n- [X] Buy milk\n- [X] Buy socks\n',
|
||||||
},
|
},
|
||||||
|
@ -31,6 +42,11 @@ describe('components', () => {
|
||||||
|
|
||||||
it('should toggle checkboxes when clicked', async () => {
|
it('should toggle checkboxes when clicked', async () => {
|
||||||
const wrapper = render(N8nMarkdown, {
|
const wrapper = render(N8nMarkdown, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
|
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
|
||||||
},
|
},
|
||||||
|
@ -50,6 +66,11 @@ describe('components', () => {
|
||||||
|
|
||||||
it('should render inputs as plain text', () => {
|
it('should render inputs as plain text', () => {
|
||||||
const wrapper = render(N8nMarkdown, {
|
const wrapper = render(N8nMarkdown, {
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
content:
|
content:
|
||||||
'__TODO__\n- [X] Buy milk\n- <input type="text" data-testid="text-input" value="Something"/>\n',
|
'__TODO__\n- [X] Buy milk\n- <input type="text" data-testid="text-input" value="Something"/>\n',
|
||||||
|
|
|
@ -73,7 +73,7 @@ const onClick = (event: MouseEvent) => {
|
||||||
:id="`${id}-content`"
|
:id="`${id}-content`"
|
||||||
:class="showFullContent ? $style['expanded'] : $style['truncated']"
|
:class="showFullContent ? $style['expanded'] : $style['truncated']"
|
||||||
role="region"
|
role="region"
|
||||||
v-html="displayContent"
|
v-n8n-html="displayContent"
|
||||||
/>
|
/>
|
||||||
</slot>
|
</slot>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { render } from '@testing-library/vue';
|
import { render } from '@testing-library/vue';
|
||||||
import N8nNotice from '../Notice.vue';
|
import N8nNotice from '../Notice.vue';
|
||||||
import { N8nText } from 'n8n-design-system/components';
|
import { N8nText } from 'n8n-design-system/components';
|
||||||
|
import { n8nHtml } from 'n8n-design-system/directives';
|
||||||
|
|
||||||
describe('components', () => {
|
describe('components', () => {
|
||||||
describe('N8nNotice', () => {
|
describe('N8nNotice', () => {
|
||||||
|
@ -41,6 +42,9 @@ describe('components', () => {
|
||||||
content: '<strong>Hello world!</strong> This is a notice.',
|
content: '<strong>Hello world!</strong> This is a notice.',
|
||||||
},
|
},
|
||||||
global: {
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
components: {
|
components: {
|
||||||
'n8n-text': N8nText,
|
'n8n-text': N8nText,
|
||||||
},
|
},
|
||||||
|
|
|
@ -116,7 +116,7 @@ const onInputScroll = (event: WheelEvent) => {
|
||||||
</div>
|
</div>
|
||||||
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
|
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
|
||||||
<N8nText size="xsmall" align="right">
|
<N8nText size="xsmall" align="right">
|
||||||
<span v-html="t('sticky.markdownHint')"></span>
|
<span v-n8n-html="t('sticky.markdownHint')"></span>
|
||||||
</N8nText>
|
</N8nText>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -89,7 +89,7 @@ const scrollRight = () => scroll(50);
|
||||||
>
|
>
|
||||||
<N8nTooltip :disabled="!option.tooltip" placement="bottom">
|
<N8nTooltip :disabled="!option.tooltip" placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div @click="handleTooltipClick(option.value, $event)" v-html="option.tooltip" />
|
<div @click="handleTooltipClick(option.value, $event)" v-n8n-html="option.tooltip" />
|
||||||
</template>
|
</template>
|
||||||
<a
|
<a
|
||||||
v-if="option.href"
|
v-if="option.href"
|
||||||
|
|
|
@ -42,7 +42,7 @@ defineOptions({
|
||||||
<slot />
|
<slot />
|
||||||
<template #content>
|
<template #content>
|
||||||
<slot name="content">
|
<slot name="content">
|
||||||
<div v-html="props.content"></div>
|
<div v-n8n-html="props.content"></div>
|
||||||
</slot>
|
</slot>
|
||||||
<div
|
<div
|
||||||
v-if="props.buttons.length"
|
v-if="props.buttons.length"
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
export { n8nTruncate } from './n8n-truncate';
|
export { n8nTruncate } from './n8n-truncate';
|
||||||
|
export { n8nHtml } from './n8n-html';
|
||||||
|
|
45
packages/design-system/src/directives/n8n-html.test.ts
Normal file
45
packages/design-system/src/directives/n8n-html.test.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { render } from '@testing-library/vue';
|
||||||
|
import { n8nHtml } from './n8n-html';
|
||||||
|
|
||||||
|
const TestComponent = {
|
||||||
|
props: {
|
||||||
|
html: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
template: '<div v-n8n-html="html"></div>',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Directive n8n-html', () => {
|
||||||
|
it('should sanitize html', async () => {
|
||||||
|
const { html } = render(TestComponent, {
|
||||||
|
props: {
|
||||||
|
html: '<span>text</span><a href="https://malicious.com" onclick="alert(1)">malicious</a><img alt="Ok" src="./images/logo.svg" onerror="alert(2)" /><script>alert(3)</script>',
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(html()).toBe(
|
||||||
|
'<div><span>text</span><a href="https://malicious.com">malicious</a><img alt="Ok" src="./images/logo.svg"></div>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not touch safe html', async () => {
|
||||||
|
const { html } = render(TestComponent, {
|
||||||
|
props: {
|
||||||
|
html: '<span>text</span><a href="https://safe.com">safe</a><img alt="Ok" src="./images/logo.svg" />',
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
directives: {
|
||||||
|
n8nHtml,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(html()).toBe(
|
||||||
|
'<div><span>text</span><a href="https://safe.com">safe</a><img alt="Ok" src="./images/logo.svg"></div>',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
37
packages/design-system/src/directives/n8n-html.ts
Normal file
37
packages/design-system/src/directives/n8n-html.ts
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
import sanitize from 'sanitize-html';
|
||||||
|
import type { DirectiveBinding, ObjectDirective } from 'vue';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom directive `n8nHtml` to replace v-html from Vue to sanitize content.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* In your Vue template, use the directive `v-n8n-html` passing the unsafe HTML.
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* <p v-n8n-html="'<a href="https://site.com" onclick="alert(1)">link</a>'">
|
||||||
|
*
|
||||||
|
* Compiles to: <p><a href="https://site.com">link</a></p>
|
||||||
|
*
|
||||||
|
* Hint: Do not use it on components
|
||||||
|
* https://vuejs.org/guide/reusability/custom-directives#usage-on-components
|
||||||
|
*/
|
||||||
|
|
||||||
|
const configuredSanitize = (html: string) =>
|
||||||
|
sanitize(html, {
|
||||||
|
allowedTags: sanitize.defaults.allowedTags.concat(['img', 'input']),
|
||||||
|
allowedAttributes: {
|
||||||
|
...sanitize.defaults.allowedAttributes,
|
||||||
|
input: ['type', 'id', 'checked'],
|
||||||
|
code: ['class'],
|
||||||
|
a: sanitize.defaults.allowedAttributes.a.concat(['data-*']),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const n8nHtml: ObjectDirective = {
|
||||||
|
beforeMount(el: HTMLElement, binding: DirectiveBinding<string>) {
|
||||||
|
el.innerHTML = configuredSanitize(binding.value);
|
||||||
|
},
|
||||||
|
beforeUpdate(el: HTMLElement, binding: DirectiveBinding<string>) {
|
||||||
|
el.innerHTML = configuredSanitize(binding.value);
|
||||||
|
},
|
||||||
|
};
|
|
@ -445,7 +445,7 @@ async function onAskAssistantClick() {
|
||||||
v-if="error.description || error.context?.descriptionKey"
|
v-if="error.description || error.context?.descriptionKey"
|
||||||
data-test-id="node-error-description"
|
data-test-id="node-error-description"
|
||||||
class="node-error-view__header-description"
|
class="node-error-view__header-description"
|
||||||
v-html="getErrorDescription()"
|
v-n8n-html="getErrorDescription()"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
v-if="isAskAssistantAvailable"
|
v-if="isAskAssistantAvailable"
|
||||||
|
|
|
@ -166,7 +166,7 @@ async function onDrop(expression: string, event: MouseEvent) {
|
||||||
<N8nText
|
<N8nText
|
||||||
:class="$style.tip"
|
:class="$style.tip"
|
||||||
size="small"
|
size="small"
|
||||||
v-html="i18n.baseText('expressionTip.javascript')"
|
v-n8n-html="i18n.baseText('expressionTip.javascript')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -56,7 +56,7 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
<div v-if="featureInfo.infoText" class="mb-l">
|
<div v-if="featureInfo.infoText" class="mb-l">
|
||||||
<n8n-info-tip theme="info" type="note">
|
<n8n-info-tip theme="info" type="note">
|
||||||
<span v-html="$locale.baseText(featureInfo.infoText)"></span>
|
<span v-n8n-html="$locale.baseText(featureInfo.infoText)"></span>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.actionBoxContainer">
|
<div :class="$style.actionBoxContainer">
|
||||||
|
@ -68,7 +68,7 @@ export default defineComponent({
|
||||||
@click:button="openLinkPage"
|
@click:button="openLinkPage"
|
||||||
>
|
>
|
||||||
<template #heading>
|
<template #heading>
|
||||||
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
|
<span v-n8n-html="$locale.baseText(featureInfo.actionBoxTitle)" />
|
||||||
</template>
|
</template>
|
||||||
</n8n-action-box>
|
</n8n-action-box>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -115,15 +115,15 @@ watchDebounced(
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="tip === 'dotPrimitive'" :class="$style.content">
|
<div v-else-if="tip === 'dotPrimitive'" :class="$style.content">
|
||||||
<span v-html="i18n.baseText('expressionTip.typeDotPrimitive')" />
|
<span v-n8n-html="i18n.baseText('expressionTip.typeDotPrimitive')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else-if="tip === 'dotObject'" :class="$style.content">
|
<div v-else-if="tip === 'dotObject'" :class="$style.content">
|
||||||
<span v-html="i18n.baseText('expressionTip.typeDotObject')" />
|
<span v-n8n-html="i18n.baseText('expressionTip.typeDotObject')" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else :class="$style.content">
|
<div v-else :class="$style.content">
|
||||||
<span v-html="i18n.baseText('expressionTip.javascript')" />
|
<span v-n8n-html="i18n.baseText('expressionTip.javascript')" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -406,7 +406,7 @@ export default defineComponent({
|
||||||
<n8n-tooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
|
<n8n-tooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div
|
<div
|
||||||
v-html="
|
v-n8n-html="
|
||||||
$locale.baseText('dataMapping.dragFromPreviousHint', {
|
$locale.baseText('dataMapping.dragFromPreviousHint', {
|
||||||
interpolate: { name: focusedMappableInput },
|
interpolate: { name: focusedMappableInput },
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,7 +12,7 @@ withDefaults(defineProps<Props>(), {
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]"
|
:class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]"
|
||||||
v-html="html"
|
v-n8n-html="html"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
|
@ -633,7 +633,7 @@ function openContextMenu(event: MouseEvent, source: 'node-button' | 'node-right-
|
||||||
<i v-if="isTriggerNode" class="trigger-icon">
|
<i v-if="isTriggerNode" class="trigger-icon">
|
||||||
<n8n-tooltip placement="bottom">
|
<n8n-tooltip placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span v-html="i18n.baseText('node.thisIsATriggerNode')" />
|
<span v-n8n-html="i18n.baseText('node.thisIsATriggerNode')" />
|
||||||
</template>
|
</template>
|
||||||
<FontAwesomeIcon icon="bolt" size="lg" />
|
<FontAwesomeIcon icon="bolt" size="lg" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
|
|
|
@ -163,7 +163,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
|
||||||
<p
|
<p
|
||||||
:class="$style.communityNodeIcon"
|
:class="$style.communityNodeIcon"
|
||||||
@click="onCommunityNodeTooltipClick"
|
@click="onCommunityNodeTooltipClick"
|
||||||
v-html="
|
v-n8n-html="
|
||||||
i18n.baseText('generic.communityNode.tooltip', {
|
i18n.baseText('generic.communityNode.tooltip', {
|
||||||
interpolate: {
|
interpolate: {
|
||||||
packageName: nodeType.name.split('.')[0],
|
packageName: nodeType.name.split('.')[0],
|
||||||
|
|
|
@ -258,7 +258,7 @@ onMounted(() => {
|
||||||
data-test-id="actions-panel-no-triggers-callout"
|
data-test-id="actions-panel-no-triggers-callout"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-html="
|
v-n8n-html="
|
||||||
i18n.baseText('nodeCreator.actionsCallout.noTriggerItems', {
|
i18n.baseText('nodeCreator.actionsCallout.noTriggerItems', {
|
||||||
interpolate: { nodeName: subcategory ?? '' },
|
interpolate: { nodeName: subcategory ?? '' },
|
||||||
})
|
})
|
||||||
|
@ -271,7 +271,7 @@ onMounted(() => {
|
||||||
<p
|
<p
|
||||||
:class="$style.resetSearch"
|
:class="$style.resetSearch"
|
||||||
@click="resetSearch"
|
@click="resetSearch"
|
||||||
v-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
|
v-n8n-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</CategorizedItemsRenderer>
|
</CategorizedItemsRenderer>
|
||||||
|
@ -293,13 +293,13 @@ onMounted(() => {
|
||||||
slim
|
slim
|
||||||
data-test-id="actions-panel-activation-callout"
|
data-test-id="actions-panel-activation-callout"
|
||||||
>
|
>
|
||||||
<span v-html="i18n.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" />
|
<span v-n8n-html="i18n.baseText('nodeCreator.actionsCallout.triggersStartWorkflow')" />
|
||||||
</n8n-callout>
|
</n8n-callout>
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<template #empty>
|
<template #empty>
|
||||||
<n8n-info-tip v-if="!search" theme="info" type="note" :class="$style.actionsEmpty">
|
<n8n-info-tip v-if="!search" theme="info" type="note" :class="$style.actionsEmpty">
|
||||||
<span
|
<span
|
||||||
v-html="
|
v-n8n-html="
|
||||||
i18n.baseText('nodeCreator.actionsCallout.noActionItems', {
|
i18n.baseText('nodeCreator.actionsCallout.noActionItems', {
|
||||||
interpolate: { nodeName: subcategory ?? '' },
|
interpolate: { nodeName: subcategory ?? '' },
|
||||||
})
|
})
|
||||||
|
@ -311,7 +311,7 @@ onMounted(() => {
|
||||||
:class="$style.resetSearch"
|
:class="$style.resetSearch"
|
||||||
data-test-id="actions-panel-no-matching-actions"
|
data-test-id="actions-panel-no-matching-actions"
|
||||||
@click="resetSearch"
|
@click="resetSearch"
|
||||||
v-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingActions')"
|
v-n8n-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingActions')"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</CategorizedItemsRenderer>
|
</CategorizedItemsRenderer>
|
||||||
|
@ -320,7 +320,7 @@ onMounted(() => {
|
||||||
<div v-if="containsAPIAction" :class="$style.apiHint">
|
<div v-if="containsAPIAction" :class="$style.apiHint">
|
||||||
<span
|
<span
|
||||||
@click.prevent="addHttpNode"
|
@click.prevent="addHttpNode"
|
||||||
v-html="
|
v-n8n-html="
|
||||||
i18n.baseText('nodeCreator.actionsList.apiCall', {
|
i18n.baseText('nodeCreator.actionsList.apiCall', {
|
||||||
interpolate: { node: subcategory ?? '' },
|
interpolate: { node: subcategory ?? '' },
|
||||||
})
|
})
|
||||||
|
|
|
@ -106,7 +106,7 @@ registerKeyHook(`CategoryLeft_${props.category}`, {
|
||||||
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
|
||||||
<n8n-icon icon="question-circle" size="small" />
|
<n8n-icon icon="question-circle" size="small" />
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-html="mouseOverTooltip" />
|
<div v-n8n-html="mouseOverTooltip" />
|
||||||
</template>
|
</template>
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -1391,7 +1391,7 @@ onUpdated(async () => {
|
||||||
<div
|
<div
|
||||||
v-if="option.description"
|
v-if="option.description"
|
||||||
class="option-description"
|
class="option-description"
|
||||||
v-html="getOptionsOptionDescription(option)"
|
v-n8n-html="getOptionsOptionDescription(option)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</n8n-option>
|
</n8n-option>
|
||||||
|
@ -1424,7 +1424,7 @@ onUpdated(async () => {
|
||||||
<div
|
<div
|
||||||
v-if="option.description"
|
v-if="option.description"
|
||||||
class="option-description"
|
class="option-description"
|
||||||
v-html="getOptionsOptionDescription(option)"
|
v-n8n-html="getOptionsOptionDescription(option)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</n8n-option>
|
</n8n-option>
|
||||||
|
|
|
@ -46,13 +46,13 @@ const simplyText = computed(() => {
|
||||||
[$style.highlight]: highlight,
|
[$style.highlight]: highlight,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<span data-test-id="parameter-input-hint" v-html="simplyText"></span>
|
<span data-test-id="parameter-input-hint" v-n8n-html="simplyText"></span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else
|
||||||
ref="hintTextRef"
|
ref="hintTextRef"
|
||||||
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
|
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
|
||||||
v-html="sanitizeHtml(hint)"
|
v-n8n-html="sanitizeHtml(hint)"
|
||||||
></div>
|
></div>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default defineComponent({
|
||||||
<div v-if="!rootStore.pushConnectionActive" class="push-connection-lost primary-color">
|
<div v-if="!rootStore.pushConnectionActive" class="push-connection-lost primary-color">
|
||||||
<n8n-tooltip placement="bottom-end">
|
<n8n-tooltip placement="bottom-end">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-html="$locale.baseText('pushConnectionTracker.cannotConnectToServer')"></div>
|
<div v-n8n-html="$locale.baseText('pushConnectionTracker.cannotConnectToServer')"></div>
|
||||||
</template>
|
</template>
|
||||||
<span>
|
<span>
|
||||||
<font-awesome-icon icon="exclamation-triangle" />
|
<font-awesome-icon icon="exclamation-triangle" />
|
||||||
|
|
|
@ -131,7 +131,7 @@ defineExpose({
|
||||||
<div class="option-headline">
|
<div class="option-headline">
|
||||||
{{ option.name }}
|
{{ option.name }}
|
||||||
</div>
|
</div>
|
||||||
<div class="option-description" v-html="option.description" />
|
<div class="option-description" v-n8n-html="option.description" />
|
||||||
</div>
|
</div>
|
||||||
</N8nOption>
|
</N8nOption>
|
||||||
</N8nSelect>
|
</N8nSelect>
|
||||||
|
|
|
@ -1350,7 +1350,7 @@ export default defineComponent({
|
||||||
:class="$style.hintCallout"
|
:class="$style.hintCallout"
|
||||||
:theme="hint.type || 'info'"
|
:theme="hint.type || 'info'"
|
||||||
>
|
>
|
||||||
<n8n-text size="small" v-html="hint.message"></n8n-text>
|
<n8n-text size="small" v-n8n-html="hint.message"></n8n-text>
|
||||||
</n8n-callout>
|
</n8n-callout>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -1509,7 +1509,7 @@ export default defineComponent({
|
||||||
<n8n-text :bold="true" color="text-dark" size="large">{{ tooMuchDataTitle }}</n8n-text>
|
<n8n-text :bold="true" color="text-dark" size="large">{{ tooMuchDataTitle }}</n8n-text>
|
||||||
<n8n-text align="center" tag="div"
|
<n8n-text align="center" tag="div"
|
||||||
><span
|
><span
|
||||||
v-html="
|
v-n8n-html="
|
||||||
$locale.baseText('ndv.output.tooMuchData.message', {
|
$locale.baseText('ndv.output.tooMuchData.message', {
|
||||||
interpolate: { size: dataSizeInMB },
|
interpolate: { size: dataSizeInMB },
|
||||||
})
|
})
|
||||||
|
|
|
@ -41,7 +41,7 @@ const runMetadata = computed(() => {
|
||||||
data-test-id="node-run-info-stale"
|
data-test-id="node-run-info-stale"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
v-html="
|
v-n8n-html="
|
||||||
i18n.baseText(
|
i18n.baseText(
|
||||||
hasPinData
|
hasPinData
|
||||||
? 'ndv.output.staleDataWarning.pinData'
|
? 'ndv.output.staleDataWarning.pinData'
|
||||||
|
|
|
@ -9,7 +9,7 @@ defineProps<{
|
||||||
<div class="titled-list">
|
<div class="titled-list">
|
||||||
<p v-text="title" />
|
<p v-text="title" />
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="item in items" :key="item" class="titled-list-item" v-html="item" />
|
<li v-for="item in items" :key="item" class="titled-list-item" v-n8n-html="item" />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -441,7 +441,7 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<n8n-text v-if="activationHint" size="small" @click="onLinkClick">
|
<n8n-text v-if="activationHint" size="small" @click="onLinkClick">
|
||||||
<span v-html="activationHint"></span>
|
<span v-n8n-html="activationHint"></span>
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
<n8n-link
|
<n8n-link
|
||||||
v-if="activationHint && executionsHelp"
|
v-if="activationHint && executionsHelp"
|
||||||
|
|
|
@ -31,7 +31,7 @@ const nodeName = (node: IVersionNode): string => {
|
||||||
{{ `${$locale.baseText('versionCard.version')} ${version.name}` }}
|
{{ `${$locale.baseText('versionCard.version')} ${version.name}` }}
|
||||||
</div>
|
</div>
|
||||||
<WarningTooltip v-if="version.hasSecurityIssue">
|
<WarningTooltip v-if="version.hasSecurityIssue">
|
||||||
<span v-html="$locale.baseText('versionCard.thisVersionHasASecurityIssue')"></span>
|
<span v-n8n-html="$locale.baseText('versionCard.thisVersionHasASecurityIssue')"></span>
|
||||||
</WarningTooltip>
|
</WarningTooltip>
|
||||||
<Badge
|
<Badge
|
||||||
v-if="version.hasSecurityFix"
|
v-if="version.hasSecurityFix"
|
||||||
|
@ -56,7 +56,7 @@ const nodeName = (node: IVersionNode): string => {
|
||||||
<div
|
<div
|
||||||
v-if="version.description"
|
v-if="version.description"
|
||||||
:class="$style.description"
|
:class="$style.description"
|
||||||
v-html="version.description"
|
v-n8n-html="version.description"
|
||||||
></div>
|
></div>
|
||||||
<div v-if="version.nodes && version.nodes.length > 0" :class="$style.nodes">
|
<div v-if="version.nodes && version.nodes.length > 0" :class="$style.nodes">
|
||||||
<NodeIcon
|
<NodeIcon
|
||||||
|
|
|
@ -138,7 +138,7 @@ async function displayActivationError() {
|
||||||
<template #content>
|
<template #content>
|
||||||
<div
|
<div
|
||||||
@click="displayActivationError"
|
@click="displayActivationError"
|
||||||
v-html="i18n.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
|
v-n8n-html="i18n.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
|
||||||
></div>
|
></div>
|
||||||
</template>
|
</template>
|
||||||
<font-awesome-icon icon="exclamation-triangle" @click="displayActivationError" />
|
<font-awesome-icon icon="exclamation-triangle" @click="displayActivationError" />
|
||||||
|
|
|
@ -573,7 +573,7 @@ export default defineComponent({
|
||||||
{{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }}
|
{{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }}
|
||||||
<n8n-tooltip placement="top">
|
<n8n-tooltip placement="top">
|
||||||
<template #content>
|
<template #content>
|
||||||
<div v-html="helpTexts.errorWorkflow"></div>
|
<div v-n8n-html="helpTexts.errorWorkflow"></div>
|
||||||
</template>
|
</template>
|
||||||
<font-awesome-icon icon="question-circle" />
|
<font-awesome-icon icon="question-circle" />
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
|
|
|
@ -17,14 +17,14 @@ const hasOwnerPermission = computed(() => hasPermission(['instanceOwner']));
|
||||||
<template>
|
<template>
|
||||||
<BaseBanner custom-icon="info-circle" theme="warning" name="V1" :class="$style.v1container">
|
<BaseBanner custom-icon="info-circle" theme="warning" name="V1" :class="$style.v1container">
|
||||||
<template #mainContent>
|
<template #mainContent>
|
||||||
<span v-html="locale.baseText('banners.v1.message')"></span>
|
<span v-n8n-html="locale.baseText('banners.v1.message')"></span>
|
||||||
<a
|
<a
|
||||||
v-if="hasOwnerPermission"
|
v-if="hasOwnerPermission"
|
||||||
:class="$style.link"
|
:class="$style.link"
|
||||||
data-test-id="banner-confirm-v1"
|
data-test-id="banner-confirm-v1"
|
||||||
@click="dismissPermanently"
|
@click="dismissPermanently"
|
||||||
>
|
>
|
||||||
<span v-html="locale.baseText('generic.dontShowAgain')"></span>
|
<span v-n8n-html="locale.baseText('generic.dontShowAgain')"></span>
|
||||||
</a>
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</BaseBanner>
|
</BaseBanner>
|
||||||
|
|
|
@ -1,10 +1,12 @@
|
||||||
import { render } from '@testing-library/vue';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import V1Banner from '../V1Banner.vue';
|
import V1Banner from '../V1Banner.vue';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
import { createPinia, setActivePinia } from 'pinia';
|
||||||
import { useUsersStore } from '@/stores/users.store';
|
import { useUsersStore } from '@/stores/users.store';
|
||||||
import { ROLE } from '@/constants';
|
import { ROLE } from '@/constants';
|
||||||
import type { IUser } from '@/Interface';
|
import type { IUser } from '@/Interface';
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(V1Banner);
|
||||||
|
|
||||||
describe('V1 Banner', () => {
|
describe('V1 Banner', () => {
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
let pinia: ReturnType<typeof createPinia>;
|
||||||
let usersStore: ReturnType<typeof useUsersStore>;
|
let usersStore: ReturnType<typeof useUsersStore>;
|
||||||
|
@ -17,7 +19,7 @@ describe('V1 Banner', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render banner', () => {
|
it('should render banner', () => {
|
||||||
const { container } = render(V1Banner);
|
const { container } = renderComponent();
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
expect(container.querySelectorAll('a')).toHaveLength(1);
|
expect(container.querySelectorAll('a')).toHaveLength(1);
|
||||||
});
|
});
|
||||||
|
@ -26,7 +28,7 @@ describe('V1 Banner', () => {
|
||||||
usersStore.usersById = { '1': { role: ROLE.Owner } as IUser };
|
usersStore.usersById = { '1': { role: ROLE.Owner } as IUser };
|
||||||
usersStore.currentUserId = '1';
|
usersStore.currentUserId = '1';
|
||||||
|
|
||||||
const { container } = render(V1Banner);
|
const { container } = renderComponent();
|
||||||
expect(container).toMatchSnapshot();
|
expect(container).toMatchSnapshot();
|
||||||
expect(container.querySelectorAll('a')).toHaveLength(2);
|
expect(container.querySelectorAll('a')).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,69 +2,209 @@
|
||||||
|
|
||||||
exports[`V1 Banner > should render banner 1`] = `
|
exports[`V1 Banner > should render banner 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<n8n-callout
|
<div
|
||||||
class="callout v1container"
|
class="n8n-callout callout warning callout v1container"
|
||||||
data-test-id="banners-V1"
|
data-test-id="banners-V1"
|
||||||
icon="info-circle"
|
role="alert"
|
||||||
icon-size="medium"
|
|
||||||
round-corners="false"
|
|
||||||
theme="warning"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mainContent keepSpace"
|
class="messageSection"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
<span>
|
class="icon"
|
||||||
n8n has been updated to version 1, introducing some breaking changes. Please consult the
|
>
|
||||||
<a
|
<span
|
||||||
href="https://docs.n8n.io/1-0-migration-checklist"
|
class="n8n-text compact size-medium regular n8n-icon n8n-icon"
|
||||||
target="_blank"
|
|
||||||
>
|
>
|
||||||
migration guide
|
|
||||||
</a>
|
<svg
|
||||||
for more information.
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-info-circle fa-w-16 medium"
|
||||||
|
data-icon="info-circle"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
class=""
|
||||||
|
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
class="n8n-text size-small regular"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mainContent keepSpace"
|
||||||
|
>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
n8n has been updated to version 1, introducing some breaking changes. Please consult the
|
||||||
|
<a
|
||||||
|
href="https://docs.n8n.io/1-0-migration-checklist"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
migration guide
|
||||||
|
</a>
|
||||||
|
for more information.
|
||||||
|
</span>
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
<!--v-if-->
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</n8n-callout>
|
|
||||||
|
<div
|
||||||
|
class="trailingContent"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="n8n-text compact size-small regular n8n-icon clickable clickable n8n-icon clickable clickable"
|
||||||
|
data-test-id="banner-V1-close"
|
||||||
|
title="Dismiss"
|
||||||
|
>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-times fa-w-11 small"
|
||||||
|
data-icon="times"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 352 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
class=""
|
||||||
|
d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`V1 Banner > should render banner with dismiss call if user is owner 1`] = `
|
exports[`V1 Banner > should render banner with dismiss call if user is owner 1`] = `
|
||||||
<div>
|
<div>
|
||||||
<n8n-callout
|
<div
|
||||||
class="callout v1container"
|
class="n8n-callout callout warning callout v1container"
|
||||||
data-test-id="banners-V1"
|
data-test-id="banners-V1"
|
||||||
icon="info-circle"
|
role="alert"
|
||||||
icon-size="medium"
|
|
||||||
round-corners="false"
|
|
||||||
theme="warning"
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="mainContent keepSpace"
|
class="messageSection"
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
<span>
|
class="icon"
|
||||||
n8n has been updated to version 1, introducing some breaking changes. Please consult the
|
|
||||||
<a
|
|
||||||
href="https://docs.n8n.io/1-0-migration-checklist"
|
|
||||||
target="_blank"
|
|
||||||
>
|
|
||||||
migration guide
|
|
||||||
</a>
|
|
||||||
for more information.
|
|
||||||
</span>
|
|
||||||
<a
|
|
||||||
class="link"
|
|
||||||
data-test-id="banner-confirm-v1"
|
|
||||||
>
|
>
|
||||||
<span>
|
<span
|
||||||
Don't show again
|
class="n8n-text compact size-medium regular n8n-icon n8n-icon"
|
||||||
|
>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-info-circle fa-w-16 medium"
|
||||||
|
data-icon="info-circle"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
class=""
|
||||||
|
d="M256 8C119.043 8 8 119.083 8 256c0 136.997 111.043 248 248 248s248-111.003 248-248C504 119.083 392.957 8 256 8zm0 110c23.196 0 42 18.804 42 42s-18.804 42-42 42-42-18.804-42-42 18.804-42 42-42zm56 254c0 6.627-5.373 12-12 12h-88c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h12v-64h-12c-6.627 0-12-5.373-12-12v-24c0-6.627 5.373-12 12-12h64c6.627 0 12 5.373 12 12v100h12c6.627 0 12 5.373 12 12v24z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</div>
|
||||||
|
<span
|
||||||
|
class="n8n-text size-small regular"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="mainContent keepSpace"
|
||||||
|
>
|
||||||
|
|
||||||
|
<span>
|
||||||
|
n8n has been updated to version 1, introducing some breaking changes. Please consult the
|
||||||
|
<a
|
||||||
|
href="https://docs.n8n.io/1-0-migration-checklist"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
migration guide
|
||||||
|
</a>
|
||||||
|
for more information.
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
class="link"
|
||||||
|
data-test-id="banner-confirm-v1"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Don't show again
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</n8n-callout>
|
|
||||||
|
<div
|
||||||
|
class="trailingContent"
|
||||||
|
>
|
||||||
|
|
||||||
|
|
||||||
|
<span
|
||||||
|
class="n8n-text compact size-small regular n8n-icon clickable clickable n8n-icon clickable clickable"
|
||||||
|
data-test-id="banner-V1-close"
|
||||||
|
title="Dismiss"
|
||||||
|
>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
class="svg-inline--fa fa-times fa-w-11 small"
|
||||||
|
data-icon="times"
|
||||||
|
data-prefix="fas"
|
||||||
|
focusable="false"
|
||||||
|
role="img"
|
||||||
|
viewBox="0 0 352 512"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
class=""
|
||||||
|
d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -96,7 +96,7 @@ function openContextMenu(event: MouseEvent) {
|
||||||
<slot />
|
<slot />
|
||||||
<N8nTooltip v-if="renderOptions.trigger" placement="bottom">
|
<N8nTooltip v-if="renderOptions.trigger" placement="bottom">
|
||||||
<template #content>
|
<template #content>
|
||||||
<span v-html="$locale.baseText('node.thisIsATriggerNode')" />
|
<span v-n8n-html="$locale.baseText('node.thisIsATriggerNode')" />
|
||||||
</template>
|
</template>
|
||||||
<div :class="$style.triggerIcon">
|
<div :class="$style.triggerIcon">
|
||||||
<FontAwesomeIcon icon="bolt" size="lg" />
|
<FontAwesomeIcon icon="bolt" size="lg" />
|
||||||
|
|
|
@ -179,7 +179,7 @@ const onTagsEditEsc = () => {
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.noResultsContainer" data-test-id="execution-annotation-data-empty">
|
<div v-else :class="$style.noResultsContainer" data-test-id="execution-annotation-data-empty">
|
||||||
<n8n-text color="text-base" size="small" align="center">
|
<n8n-text color="text-base" size="small" align="center">
|
||||||
<span v-html="$locale.baseText('executionAnnotationView.data.notFound')" />
|
<span v-n8n-html="$locale.baseText('executionAnnotationView.data.notFound')" />
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -633,7 +633,7 @@ export default defineComponent({
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.docsInfoTip">
|
<div :class="$style.docsInfoTip">
|
||||||
<n8n-info-tip theme="info" type="note">
|
<n8n-info-tip theme="info" type="note">
|
||||||
<span v-html="$locale.baseText('settings.ldap.infoTip')"></span>
|
<span v-n8n-html="$locale.baseText('settings.ldap.infoTip')"></span>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.settingsForm">
|
<div :class="$style.settingsForm">
|
||||||
|
|
|
@ -175,7 +175,7 @@ export default defineComponent({
|
||||||
<template v-if="isLicensed">
|
<template v-if="isLicensed">
|
||||||
<div class="mb-l">
|
<div class="mb-l">
|
||||||
<n8n-info-tip theme="info" type="note">
|
<n8n-info-tip theme="info" type="note">
|
||||||
<span v-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
<span v-n8n-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
</div>
|
</div>
|
||||||
<template v-if="storeHasItems()">
|
<template v-if="storeHasItems()">
|
||||||
|
@ -207,7 +207,7 @@ export default defineComponent({
|
||||||
@click:button="addDestination"
|
@click:button="addDestination"
|
||||||
>
|
>
|
||||||
<template #heading>
|
<template #heading>
|
||||||
<span v-html="$locale.baseText(`settings.log-streaming.addFirstTitle`)" />
|
<span v-n8n-html="$locale.baseText(`settings.log-streaming.addFirstTitle`)" />
|
||||||
</template>
|
</template>
|
||||||
</n8n-action-box>
|
</n8n-action-box>
|
||||||
</div>
|
</div>
|
||||||
|
@ -215,7 +215,7 @@ export default defineComponent({
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="$locale.baseText('settings.log-streaming.infoText')" class="mb-l">
|
<div v-if="$locale.baseText('settings.log-streaming.infoText')" class="mb-l">
|
||||||
<n8n-info-tip theme="info" type="note">
|
<n8n-info-tip theme="info" type="note">
|
||||||
<span v-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
<span v-n8n-html="$locale.baseText('settings.log-streaming.infoText')"></span>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
</div>
|
</div>
|
||||||
<div data-test-id="action-box-unlicensed">
|
<div data-test-id="action-box-unlicensed">
|
||||||
|
@ -225,7 +225,7 @@ export default defineComponent({
|
||||||
@click:button="goToUpgrade"
|
@click:button="goToUpgrade"
|
||||||
>
|
>
|
||||||
<template #heading>
|
<template #heading>
|
||||||
<span v-html="$locale.baseText('settings.log-streaming.actionBox.title')" />
|
<span v-n8n-html="$locale.baseText('settings.log-streaming.actionBox.title')" />
|
||||||
</template>
|
</template>
|
||||||
</n8n-action-box>
|
</n8n-action-box>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -28,7 +28,7 @@ const appNodeCounts = computed(() => {
|
||||||
<template>
|
<template>
|
||||||
<N8nNotice :class="$style.notice" theme="info">
|
<N8nNotice :class="$style.notice" theme="info">
|
||||||
<i18n-t tag="span" keypath="templateSetup.instructions" scope="global">
|
<i18n-t tag="span" keypath="templateSetup.instructions" scope="global">
|
||||||
<span v-html="appNodeCounts" />
|
<span v-n8n-html="appNodeCounts" />
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</N8nNotice>
|
</N8nNotice>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -95,7 +95,7 @@ const onCredentialModalOpened = () => {
|
||||||
:plural="credentials.usedBy.length"
|
:plural="credentials.usedBy.length"
|
||||||
scope="global"
|
scope="global"
|
||||||
>
|
>
|
||||||
<span v-html="nodeNames" />
|
<span v-n8n-html="nodeNames" />
|
||||||
</i18n-t>
|
</i18n-t>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
|
|
@ -410,7 +410,7 @@ export default defineComponent({
|
||||||
/>
|
/>
|
||||||
<div v-if="endOfSearchMessage" :class="$style.endText">
|
<div v-if="endOfSearchMessage" :class="$style.endText">
|
||||||
<n8n-text size="medium" color="text-base">
|
<n8n-text size="medium" color="text-base">
|
||||||
<span v-html="endOfSearchMessage" />
|
<span v-n8n-html="endOfSearchMessage" />
|
||||||
</n8n-text>
|
</n8n-text>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1205,9 +1205,6 @@ importers:
|
||||||
'@vitest/coverage-v8':
|
'@vitest/coverage-v8':
|
||||||
specifier: catalog:frontend
|
specifier: catalog:frontend
|
||||||
version: 1.6.0(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))
|
version: 1.6.0(vitest@1.6.0(@types/node@18.16.16)(jsdom@23.0.1)(sass@1.64.1)(terser@5.16.1))
|
||||||
'@vue/test-utils':
|
|
||||||
specifier: ^2.4.3
|
|
||||||
version: 2.4.3(@vue/server-renderer@3.4.21(vue@3.4.21(typescript@5.6.2)))(vue@3.4.21(typescript@5.6.2))
|
|
||||||
autoprefixer:
|
autoprefixer:
|
||||||
specifier: ^10.4.19
|
specifier: ^10.4.19
|
||||||
version: 10.4.19(postcss@8.4.38)
|
version: 10.4.19(postcss@8.4.38)
|
||||||
|
|
Loading…
Reference in a new issue