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", "@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",

View file

@ -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'"

View file

@ -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: [

View file

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

View file

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

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

View file

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

View file

@ -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">

View file

@ -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',

View file

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

View file

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

View file

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

View file

@ -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"

View file

@ -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"

View file

@ -1 +1,2 @@
export { n8nTruncate } from './n8n-truncate'; 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" 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"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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],

View file

@ -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 ?? '' },
}) })

View file

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

View file

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

View file

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

View file

@ -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" />&nbsp; <font-awesome-icon icon="exclamation-triangle" />&nbsp;

View file

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

View file

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

View file

@ -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'

View file

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

View file

@ -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>&nbsp; <span v-n8n-html="activationHint"></span>&nbsp;
</n8n-text> </n8n-text>
<n8n-link <n8n-link
v-if="activationHint && executionsHelp" v-if="activationHint && executionsHelp"

View file

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

View file

@ -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" />

View file

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

View file

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

View file

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

View file

@ -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>
`; `;

View file

@ -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" />

View file

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

View file

@ -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">

View file

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

View file

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

View file

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

View file

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

View file

@ -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)