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:
Csaba Tuncsik 2024-09-18 08:49:41 +02:00 committed by GitHub
parent 989f69d1f4
commit 44e5fb9b06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
49 changed files with 379 additions and 130 deletions

View file

@ -29,7 +29,6 @@
"@types/sanitize-html": "^2.11.0",
"@vitejs/plugin-vue": "^5.0.4",
"@vitest/coverage-v8": "catalog:frontend",
"@vue/test-utils": "^2.4.3",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.38",
"sass": "^1.64.1",

View file

@ -151,8 +151,7 @@ function growInput() {
/>
</div>
<div :class="$style.blockBody">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-html="renderMarkdown(message.content)"></span>
<span v-n8n-html="renderMarkdown(message.content)"></span>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.title && message.content"
/>
@ -160,19 +159,20 @@ function growInput() {
</div>
</div>
<div v-else-if="message.type === 'text'" :class="$style.textMessage">
<!-- eslint-disable-next-line vue/no-v-html -->
<span v-if="message.role === 'user'" v-html="renderMarkdown(message.content)"></span>
<!-- eslint-disable-next-line vue/no-v-html -->
<span
v-if="message.role === 'user'"
v-n8n-html="renderMarkdown(message.content)"
></span>
<div
v-else
v-n8n-html="renderMarkdown(message.content)"
:class="$style.assistantText"
v-html="renderMarkdown(message.content)"
></div>
<div
v-if="message?.codeSnippet"
:class="$style['code-snippet']"
data-test-id="assistant-code-snippet"
v-html="renderMarkdown(message.codeSnippet).trim()"
v-n8n-html="renderMarkdown(message.codeSnippet).trim()"
></div>
<BlinkingCursor
v-if="streaming && i === messages?.length - 1 && message.role === 'assistant'"

View file

@ -1,5 +1,6 @@
import { render } from '@testing-library/vue';
import AskAssistantChat from '../AskAssistantChat.vue';
import { n8nHtml } from 'n8n-design-system/directives';
describe('AskAssistantChat', () => {
it('renders default placeholder chat correctly', () => {
@ -12,6 +13,11 @@ describe('AskAssistantChat', () => {
});
it('renders chat with messages correctly', () => {
const { container } = render(AskAssistantChat, {
global: {
directives: {
n8nHtml,
},
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages: [
@ -86,6 +92,11 @@ describe('AskAssistantChat', () => {
});
it('renders streaming chat correctly', () => {
const { container } = render(AskAssistantChat, {
global: {
directives: {
n8nHtml,
},
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages: [
@ -105,6 +116,11 @@ describe('AskAssistantChat', () => {
});
it('renders end of session chat correctly', () => {
const { container } = render(AskAssistantChat, {
global: {
directives: {
n8nHtml,
},
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages: [
@ -130,6 +146,11 @@ describe('AskAssistantChat', () => {
});
it('renders message with code snippet', () => {
const { container } = render(AskAssistantChat, {
global: {
directives: {
n8nHtml,
},
},
props: {
user: { firstName: 'Kobi', lastName: 'Dog' },
messages: [

View file

@ -129,9 +129,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<div
class="textMessage"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div
class="assistantText"
>
@ -145,7 +142,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
</div>
<!--v-if-->
<!--v-if-->
</div>
@ -438,7 +434,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<div
class="textMessage"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<span>
<p>
Give it to me
@ -516,7 +511,6 @@ exports[`AskAssistantChat > renders chat with messages correctly 1`] = `
<div
class="blockBody"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<span>
<p>
Solution steps:
@ -1060,9 +1054,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
<div
class="textMessage"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div
class="assistantText"
>
@ -1076,7 +1067,6 @@ exports[`AskAssistantChat > renders end of session chat correctly 1`] = `
</div>
<!--v-if-->
<!--v-if-->
</div>
@ -1309,9 +1299,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
<div
class="textMessage"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div
class="assistantText"
>
@ -1325,7 +1312,6 @@ exports[`AskAssistantChat > renders message with code snippet 1`] = `
</div>
<div
class="code-snippet"
data-test-id="assistant-code-snippet"
@ -1552,9 +1538,6 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
<div
class="textMessage"
>
<!-- eslint-disable-next-line vue/no-v-html -->
<!-- eslint-disable-next-line vue/no-v-html -->
<div
class="assistantText"
>
@ -1568,7 +1551,6 @@ exports[`AskAssistantChat > renders streaming chat correctly 1`] = `
</div>
<!--v-if-->
<!--v-if-->
</div>

View file

@ -37,7 +37,7 @@ withDefaults(defineProps<ActionBoxProps>(), {
<div :class="$style.description" @click="$emit('descriptionClick', $event)">
<N8nText color="text-base">
<slot name="description">
<span v-html="description"></span>
<span v-n8n-html="description"></span>
</slot>
</N8nText>
</div>
@ -61,7 +61,7 @@ withDefaults(defineProps<ActionBoxProps>(), {
:class="$style.callout"
>
<N8nText color="text-base">
<span size="small" v-html="calloutText"></span>
<span size="small" v-n8n-html="calloutText"></span>
</N8nText>
</N8nCallout>
</div>

View file

@ -75,7 +75,7 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
<div v-for="item in items" :key="item.id" :class="$style.accordionItem">
<n8n-tooltip :disabled="!item.tooltip">
<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>
<N8nIcon :icon="item.icon" :color="item.iconColor" size="small" class="mr-2xs" />
</n8n-tooltip>
@ -83,7 +83,7 @@ const onTooltipClick = (item: string, event: MouseEvent) => emit('tooltipClick',
</div>
</div>
<N8nText color="text-base" size="small" align="left">
<span v-html="description"></span>
<span v-n8n-html="description"></span>
</N8nText>
<slot name="customContent"></slot>
</div>

View file

@ -58,7 +58,7 @@ const addTargetBlank = (html: string) =>
<N8nTooltip placement="top" :popper-class="$style.tooltipPopper" :show-after="300">
<N8nIcon icon="question-circle" size="small" />
<template #content>
<div v-html="addTargetBlank(tooltipText)" />
<div v-n8n-html="addTargetBlank(tooltipText)" />
</template>
</N8nTooltip>
</span>

View file

@ -202,7 +202,7 @@ const onCheckboxChange = (index: number) => {
@click="onClick"
@mousedown="onMouseDown"
@change="onChange"
v-html="htmlContent"
v-n8n-html="htmlContent"
/>
<div v-else :class="$style.markdown">
<div v-for="(_, index) in loadingBlocks" :key="index">

View file

@ -1,10 +1,16 @@
import { render, fireEvent } from '@testing-library/vue';
import N8nMarkdown from '../Markdown.vue';
import { n8nHtml } from 'n8n-design-system/directives';
describe('components', () => {
describe('N8nMarkdown', () => {
it('should render unchecked checkboxes', () => {
const wrapper = render(N8nMarkdown, {
global: {
directives: {
n8nHtml,
},
},
props: {
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
},
@ -18,6 +24,11 @@ describe('components', () => {
it('should render checked checkboxes', () => {
const wrapper = render(N8nMarkdown, {
global: {
directives: {
n8nHtml,
},
},
props: {
content: '__TODO__\n- [X] Buy milk\n- [X] Buy socks\n',
},
@ -31,6 +42,11 @@ describe('components', () => {
it('should toggle checkboxes when clicked', async () => {
const wrapper = render(N8nMarkdown, {
global: {
directives: {
n8nHtml,
},
},
props: {
content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
},
@ -50,6 +66,11 @@ describe('components', () => {
it('should render inputs as plain text', () => {
const wrapper = render(N8nMarkdown, {
global: {
directives: {
n8nHtml,
},
},
props: {
content:
'__TODO__\n- [X] Buy milk\n- <input type="text" data-testid="text-input" value="Something"/>\n',

View file

@ -73,7 +73,7 @@ const onClick = (event: MouseEvent) => {
:id="`${id}-content`"
:class="showFullContent ? $style['expanded'] : $style['truncated']"
role="region"
v-html="displayContent"
v-n8n-html="displayContent"
/>
</slot>
</N8nText>

View file

@ -1,6 +1,7 @@
import { render } from '@testing-library/vue';
import N8nNotice from '../Notice.vue';
import { N8nText } from 'n8n-design-system/components';
import { n8nHtml } from 'n8n-design-system/directives';
describe('components', () => {
describe('N8nNotice', () => {
@ -41,6 +42,9 @@ describe('components', () => {
content: '<strong>Hello world!</strong> This is a notice.',
},
global: {
directives: {
n8nHtml,
},
components: {
'n8n-text': N8nText,
},

View file

@ -116,7 +116,7 @@ const onInputScroll = (event: WheelEvent) => {
</div>
<div v-if="editMode && shouldShowFooter" :class="$style.footer">
<N8nText size="xsmall" align="right">
<span v-html="t('sticky.markdownHint')"></span>
<span v-n8n-html="t('sticky.markdownHint')"></span>
</N8nText>
</div>
</div>

View file

@ -89,7 +89,7 @@ const scrollRight = () => scroll(50);
>
<N8nTooltip :disabled="!option.tooltip" placement="bottom">
<template #content>
<div @click="handleTooltipClick(option.value, $event)" v-html="option.tooltip" />
<div @click="handleTooltipClick(option.value, $event)" v-n8n-html="option.tooltip" />
</template>
<a
v-if="option.href"

View file

@ -42,7 +42,7 @@ defineOptions({
<slot />
<template #content>
<slot name="content">
<div v-html="props.content"></div>
<div v-n8n-html="props.content"></div>
</slot>
<div
v-if="props.buttons.length"

View file

@ -1 +1,2 @@
export { n8nTruncate } from './n8n-truncate';
export { n8nHtml } from './n8n-html';

View 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>',
);
});
});

View 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);
},
};

View file

@ -445,7 +445,7 @@ async function onAskAssistantClick() {
v-if="error.description || error.context?.descriptionKey"
data-test-id="node-error-description"
class="node-error-view__header-description"
v-html="getErrorDescription()"
v-n8n-html="getErrorDescription()"
></div>
<div
v-if="isAskAssistantAvailable"

View file

@ -166,7 +166,7 @@ async function onDrop(expression: string, event: MouseEvent) {
<N8nText
:class="$style.tip"
size="small"
v-html="i18n.baseText('expressionTip.javascript')"
v-n8n-html="i18n.baseText('expressionTip.javascript')"
/>
</div>

View file

@ -56,7 +56,7 @@ export default defineComponent({
</div>
<div v-if="featureInfo.infoText" class="mb-l">
<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>
</div>
<div :class="$style.actionBoxContainer">
@ -68,7 +68,7 @@ export default defineComponent({
@click:button="openLinkPage"
>
<template #heading>
<span v-html="$locale.baseText(featureInfo.actionBoxTitle)" />
<span v-n8n-html="$locale.baseText(featureInfo.actionBoxTitle)" />
</template>
</n8n-action-box>
</div>

View file

@ -115,15 +115,15 @@ watchDebounced(
</div>
<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 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 v-else :class="$style.content">
<span v-html="i18n.baseText('expressionTip.javascript')" />
<span v-n8n-html="i18n.baseText('expressionTip.javascript')" />
</div>
</div>
</template>

View file

@ -406,7 +406,7 @@ export default defineComponent({
<n8n-tooltip v-if="!readOnly" :visible="showDraggableHint && showDraggableHintWithDelay">
<template #content>
<div
v-html="
v-n8n-html="
$locale.baseText('dataMapping.dragFromPreviousHint', {
interpolate: { name: focusedMappableInput },
})

View file

@ -12,7 +12,7 @@ withDefaults(defineProps<Props>(), {
<template>
<div
:class="[$style.dragPill, canDrop ? $style.droppablePill : $style.defaultPill]"
v-html="html"
v-n8n-html="html"
/>
</template>

View file

@ -633,7 +633,7 @@ function openContextMenu(event: MouseEvent, source: 'node-button' | 'node-right-
<i v-if="isTriggerNode" class="trigger-icon">
<n8n-tooltip placement="bottom">
<template #content>
<span v-html="i18n.baseText('node.thisIsATriggerNode')" />
<span v-n8n-html="i18n.baseText('node.thisIsATriggerNode')" />
</template>
<FontAwesomeIcon icon="bolt" size="lg" />
</n8n-tooltip>

View file

@ -163,7 +163,7 @@ function onCommunityNodeTooltipClick(event: MouseEvent) {
<p
:class="$style.communityNodeIcon"
@click="onCommunityNodeTooltipClick"
v-html="
v-n8n-html="
i18n.baseText('generic.communityNode.tooltip', {
interpolate: {
packageName: nodeType.name.split('.')[0],

View file

@ -258,7 +258,7 @@ onMounted(() => {
data-test-id="actions-panel-no-triggers-callout"
>
<span
v-html="
v-n8n-html="
i18n.baseText('nodeCreator.actionsCallout.noTriggerItems', {
interpolate: { nodeName: subcategory ?? '' },
})
@ -271,7 +271,7 @@ onMounted(() => {
<p
:class="$style.resetSearch"
@click="resetSearch"
v-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
v-n8n-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingTriggers')"
/>
</template>
</CategorizedItemsRenderer>
@ -293,13 +293,13 @@ onMounted(() => {
slim
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>
<!-- Empty state -->
<template #empty>
<n8n-info-tip v-if="!search" theme="info" type="note" :class="$style.actionsEmpty">
<span
v-html="
v-n8n-html="
i18n.baseText('nodeCreator.actionsCallout.noActionItems', {
interpolate: { nodeName: subcategory ?? '' },
})
@ -311,7 +311,7 @@ onMounted(() => {
:class="$style.resetSearch"
data-test-id="actions-panel-no-matching-actions"
@click="resetSearch"
v-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingActions')"
v-n8n-html="i18n.baseText('nodeCreator.actionsCategory.noMatchingActions')"
/>
</template>
</CategorizedItemsRenderer>
@ -320,7 +320,7 @@ onMounted(() => {
<div v-if="containsAPIAction" :class="$style.apiHint">
<span
@click.prevent="addHttpNode"
v-html="
v-n8n-html="
i18n.baseText('nodeCreator.actionsList.apiCall', {
interpolate: { node: subcategory ?? '' },
})

View file

@ -106,7 +106,7 @@ registerKeyHook(`CategoryLeft_${props.category}`, {
<n8n-tooltip placement="top" :popper-class="$style.tooltipPopper">
<n8n-icon icon="question-circle" size="small" />
<template #content>
<div v-html="mouseOverTooltip" />
<div v-n8n-html="mouseOverTooltip" />
</template>
</n8n-tooltip>
</span>

View file

@ -1391,7 +1391,7 @@ onUpdated(async () => {
<div
v-if="option.description"
class="option-description"
v-html="getOptionsOptionDescription(option)"
v-n8n-html="getOptionsOptionDescription(option)"
></div>
</div>
</n8n-option>
@ -1424,7 +1424,7 @@ onUpdated(async () => {
<div
v-if="option.description"
class="option-description"
v-html="getOptionsOptionDescription(option)"
v-n8n-html="getOptionsOptionDescription(option)"
></div>
</div>
</n8n-option>

View file

@ -46,13 +46,13 @@ const simplyText = computed(() => {
[$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
v-else
ref="hintTextRef"
:class="{ [$style.singleline]: singleLine, [$style.highlight]: highlight }"
v-html="sanitizeHtml(hint)"
v-n8n-html="sanitizeHtml(hint)"
></div>
</n8n-text>
</template>

View file

@ -16,7 +16,7 @@ export default defineComponent({
<div v-if="!rootStore.pushConnectionActive" class="push-connection-lost primary-color">
<n8n-tooltip placement="bottom-end">
<template #content>
<div v-html="$locale.baseText('pushConnectionTracker.cannotConnectToServer')"></div>
<div v-n8n-html="$locale.baseText('pushConnectionTracker.cannotConnectToServer')"></div>
</template>
<span>
<font-awesome-icon icon="exclamation-triangle" />&nbsp;

View file

@ -131,7 +131,7 @@ defineExpose({
<div class="option-headline">
{{ option.name }}
</div>
<div class="option-description" v-html="option.description" />
<div class="option-description" v-n8n-html="option.description" />
</div>
</N8nOption>
</N8nSelect>

View file

@ -1350,7 +1350,7 @@ export default defineComponent({
:class="$style.hintCallout"
: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>
<div
@ -1509,7 +1509,7 @@ export default defineComponent({
<n8n-text :bold="true" color="text-dark" size="large">{{ tooMuchDataTitle }}</n8n-text>
<n8n-text align="center" tag="div"
><span
v-html="
v-n8n-html="
$locale.baseText('ndv.output.tooMuchData.message', {
interpolate: { size: dataSizeInMB },
})

View file

@ -41,7 +41,7 @@ const runMetadata = computed(() => {
data-test-id="node-run-info-stale"
>
<span
v-html="
v-n8n-html="
i18n.baseText(
hasPinData
? 'ndv.output.staleDataWarning.pinData'

View file

@ -9,7 +9,7 @@ defineProps<{
<div class="titled-list">
<p v-text="title" />
<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>
</div>
</template>

View file

@ -441,7 +441,7 @@ export default defineComponent({
</div>
<n8n-text v-if="activationHint" size="small" @click="onLinkClick">
<span v-html="activationHint"></span>&nbsp;
<span v-n8n-html="activationHint"></span>&nbsp;
</n8n-text>
<n8n-link
v-if="activationHint && executionsHelp"

View file

@ -31,7 +31,7 @@ const nodeName = (node: IVersionNode): string => {
{{ `${$locale.baseText('versionCard.version')} ${version.name}` }}
</div>
<WarningTooltip v-if="version.hasSecurityIssue">
<span v-html="$locale.baseText('versionCard.thisVersionHasASecurityIssue')"></span>
<span v-n8n-html="$locale.baseText('versionCard.thisVersionHasASecurityIssue')"></span>
</WarningTooltip>
<Badge
v-if="version.hasSecurityFix"
@ -56,7 +56,7 @@ const nodeName = (node: IVersionNode): string => {
<div
v-if="version.description"
:class="$style.description"
v-html="version.description"
v-n8n-html="version.description"
></div>
<div v-if="version.nodes && version.nodes.length > 0" :class="$style.nodes">
<NodeIcon

View file

@ -138,7 +138,7 @@ async function displayActivationError() {
<template #content>
<div
@click="displayActivationError"
v-html="i18n.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
v-n8n-html="i18n.baseText('workflowActivator.theWorkflowIsSetToBeActiveBut')"
></div>
</template>
<font-awesome-icon icon="exclamation-triangle" @click="displayActivationError" />

View file

@ -573,7 +573,7 @@ export default defineComponent({
{{ $locale.baseText('workflowSettings.errorWorkflow') + ':' }}
<n8n-tooltip placement="top">
<template #content>
<div v-html="helpTexts.errorWorkflow"></div>
<div v-n8n-html="helpTexts.errorWorkflow"></div>
</template>
<font-awesome-icon icon="question-circle" />
</n8n-tooltip>

View file

@ -17,14 +17,14 @@ const hasOwnerPermission = computed(() => hasPermission(['instanceOwner']));
<template>
<BaseBanner custom-icon="info-circle" theme="warning" name="V1" :class="$style.v1container">
<template #mainContent>
<span v-html="locale.baseText('banners.v1.message')"></span>
<span v-n8n-html="locale.baseText('banners.v1.message')"></span>
<a
v-if="hasOwnerPermission"
:class="$style.link"
data-test-id="banner-confirm-v1"
@click="dismissPermanently"
>
<span v-html="locale.baseText('generic.dontShowAgain')"></span>
<span v-n8n-html="locale.baseText('generic.dontShowAgain')"></span>
</a>
</template>
</BaseBanner>

View file

@ -1,10 +1,12 @@
import { render } from '@testing-library/vue';
import { createComponentRenderer } from '@/__tests__/render';
import V1Banner from '../V1Banner.vue';
import { createPinia, setActivePinia } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { ROLE } from '@/constants';
import type { IUser } from '@/Interface';
const renderComponent = createComponentRenderer(V1Banner);
describe('V1 Banner', () => {
let pinia: ReturnType<typeof createPinia>;
let usersStore: ReturnType<typeof useUsersStore>;
@ -17,7 +19,7 @@ describe('V1 Banner', () => {
});
it('should render banner', () => {
const { container } = render(V1Banner);
const { container } = renderComponent();
expect(container).toMatchSnapshot();
expect(container.querySelectorAll('a')).toHaveLength(1);
});
@ -26,7 +28,7 @@ describe('V1 Banner', () => {
usersStore.usersById = { '1': { role: ROLE.Owner } as IUser };
usersStore.currentUserId = '1';
const { container } = render(V1Banner);
const { container } = renderComponent();
expect(container).toMatchSnapshot();
expect(container.querySelectorAll('a')).toHaveLength(2);
});

View file

@ -2,69 +2,209 @@
exports[`V1 Banner > should render banner 1`] = `
<div>
<n8n-callout
class="callout v1container"
<div
class="n8n-callout callout warning callout v1container"
data-test-id="banners-V1"
icon="info-circle"
icon-size="medium"
round-corners="false"
theme="warning"
role="alert"
>
<div
class="mainContent keepSpace"
class="messageSection"
>
<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"
<div
class="icon"
>
<span
class="n8n-text compact size-medium regular n8n-icon n8n-icon"
>
migration guide
</a>
for more information.
<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>
</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>
<!--v-if-->
 
</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>
`;
exports[`V1 Banner > should render banner with dismiss call if user is owner 1`] = `
<div>
<n8n-callout
class="callout v1container"
<div
class="n8n-callout callout warning callout v1container"
data-test-id="banners-V1"
icon="info-circle"
icon-size="medium"
round-corners="false"
theme="warning"
role="alert"
>
<div
class="mainContent keepSpace"
class="messageSection"
>
<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"
<div
class="icon"
>
<span>
Don't show again
<span
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>
</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>
</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>
`;

View file

@ -96,7 +96,7 @@ function openContextMenu(event: MouseEvent) {
<slot />
<N8nTooltip v-if="renderOptions.trigger" placement="bottom">
<template #content>
<span v-html="$locale.baseText('node.thisIsATriggerNode')" />
<span v-n8n-html="$locale.baseText('node.thisIsATriggerNode')" />
</template>
<div :class="$style.triggerIcon">
<FontAwesomeIcon icon="bolt" size="lg" />

View file

@ -179,7 +179,7 @@ const onTagsEditEsc = () => {
</div>
<div v-else :class="$style.noResultsContainer" data-test-id="execution-annotation-data-empty">
<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>
</div>
</div>

View file

@ -633,7 +633,7 @@ export default defineComponent({
</div>
<div :class="$style.docsInfoTip">
<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>
</div>
<div :class="$style.settingsForm">

View file

@ -175,7 +175,7 @@ export default defineComponent({
<template v-if="isLicensed">
<div class="mb-l">
<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>
</div>
<template v-if="storeHasItems()">
@ -207,7 +207,7 @@ export default defineComponent({
@click:button="addDestination"
>
<template #heading>
<span v-html="$locale.baseText(`settings.log-streaming.addFirstTitle`)" />
<span v-n8n-html="$locale.baseText(`settings.log-streaming.addFirstTitle`)" />
</template>
</n8n-action-box>
</div>
@ -215,7 +215,7 @@ export default defineComponent({
<template v-else>
<div v-if="$locale.baseText('settings.log-streaming.infoText')" class="mb-l">
<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>
</div>
<div data-test-id="action-box-unlicensed">
@ -225,7 +225,7 @@ export default defineComponent({
@click:button="goToUpgrade"
>
<template #heading>
<span v-html="$locale.baseText('settings.log-streaming.actionBox.title')" />
<span v-n8n-html="$locale.baseText('settings.log-streaming.actionBox.title')" />
</template>
</n8n-action-box>
</div>

View file

@ -28,7 +28,7 @@ const appNodeCounts = computed(() => {
<template>
<N8nNotice :class="$style.notice" theme="info">
<i18n-t tag="span" keypath="templateSetup.instructions" scope="global">
<span v-html="appNodeCounts" />
<span v-n8n-html="appNodeCounts" />
</i18n-t>
</N8nNotice>
</template>

View file

@ -95,7 +95,7 @@ const onCredentialModalOpened = () => {
:plural="credentials.usedBy.length"
scope="global"
>
<span v-html="nodeNames" />
<span v-n8n-html="nodeNames" />
</i18n-t>
</p>

View file

@ -410,7 +410,7 @@ export default defineComponent({
/>
<div v-if="endOfSearchMessage" :class="$style.endText">
<n8n-text size="medium" color="text-base">
<span v-html="endOfSearchMessage" />
<span v-n8n-html="endOfSearchMessage" />
</n8n-text>
</div>
</div>

View file

@ -1205,9 +1205,6 @@ importers:
'@vitest/coverage-v8':
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))
'@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:
specifier: ^10.4.19
version: 10.4.19(postcss@8.4.38)