diff --git a/packages/design-system/src/components/N8nMarkdown/Markdown.stories.ts b/packages/design-system/src/components/N8nMarkdown/Markdown.stories.ts
index 180bf23acd4f5..783d9eeead24c 100644
--- a/packages/design-system/src/components/N8nMarkdown/Markdown.stories.ts
+++ b/packages/design-system/src/components/N8nMarkdown/Markdown.stories.ts
@@ -51,3 +51,18 @@ Markdown.args = {
},
],
};
+
+const TemplateWithCheckboxes: StoryFn = (args, { argTypes }) => ({
+ setup: () => ({ args }),
+ props: Object.keys(argTypes),
+ components: {
+ N8nMarkdown,
+ },
+ template: '',
+});
+
+export const WithCheckboxes = TemplateWithCheckboxes.bind({});
+WithCheckboxes.args = {
+ content: '__TODO__\n- [ ] Buy milk\n- [X] Buy socks\n',
+ loading: false,
+};
diff --git a/packages/design-system/src/components/N8nMarkdown/Markdown.vue b/packages/design-system/src/components/N8nMarkdown/Markdown.vue
index 41f64ff61ee3b..ff0ad5c09f8b6 100644
--- a/packages/design-system/src/components/N8nMarkdown/Markdown.vue
+++ b/packages/design-system/src/components/N8nMarkdown/Markdown.vue
@@ -23,7 +23,7 @@ import Markdown from 'markdown-it';
import markdownLink from 'markdown-it-link-attributes';
import markdownEmoji from 'markdown-it-emoji';
import markdownTaskLists from 'markdown-it-task-lists';
-import xss, { friendlyAttrValue } from 'xss';
+import xss, { friendlyAttrValue, whiteList } from 'xss';
import N8nLoading from '../N8nLoading';
import { escapeMarkdown } from '../../utils/markdown';
@@ -72,6 +72,7 @@ const props = withDefaults(defineProps(), {
},
},
tasklists: {
+ enabled: true,
label: true,
labelAfter: true,
},
@@ -84,6 +85,11 @@ const md = new Markdown(options.markdown)
.use(markdownEmoji)
.use(markdownTaskLists, options.tasklists);
+const xssWhiteList = {
+ ...whiteList,
+ label: ['class', 'for'],
+};
+
const htmlContent = computed(() => {
if (!props.content) {
return '';
@@ -130,6 +136,13 @@ const htmlContent = computed(() => {
}
// return nothing, keep tag
},
+ onIgnoreTag(tag, tagHTML) {
+ // Allow checkboxes
+ if (tag === 'input' && tagHTML.includes('type="checkbox"')) {
+ return tagHTML;
+ }
+ },
+ whiteList: xssWhiteList,
});
return safeHtml;
diff --git a/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts b/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts
new file mode 100644
index 0000000000000..b0c76ee45d83b
--- /dev/null
+++ b/packages/design-system/src/components/N8nMarkdown/__tests__/Markdown.spec.ts
@@ -0,0 +1,45 @@
+import { render } from '@testing-library/vue';
+import N8nMarkdown from '../Markdown.vue';
+
+describe('components', () => {
+ describe('N8nMarkdown', () => {
+ it('should render unchecked checkboxes', () => {
+ const wrapper = render(N8nMarkdown, {
+ props: {
+ content: '__TODO__\n- [ ] Buy milk\n- [ ] Buy socks\n',
+ },
+ });
+ const checkboxes = wrapper.getAllByRole('checkbox');
+ expect(checkboxes).toHaveLength(2);
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).not.toBeChecked();
+ });
+ });
+ it('should render checked checkboxes', () => {
+ const wrapper = render(N8nMarkdown, {
+ props: {
+ content: '__TODO__\n- [X] Buy milk\n- [X] Buy socks\n',
+ },
+ });
+ const checkboxes = wrapper.getAllByRole('checkbox');
+ expect(checkboxes).toHaveLength(2);
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).toBeChecked();
+ });
+ });
+ it('should render inputs as plain text', () => {
+ const wrapper = render(N8nMarkdown, {
+ props: {
+ content:
+ '__TODO__\n- [X] Buy milk\n- \n',
+ },
+ });
+ const checkboxes = wrapper.getAllByRole('checkbox');
+ expect(checkboxes).toHaveLength(1);
+ expect(wrapper.queryByTestId('text-input')).toBeNull();
+ expect(wrapper.html()).toContain(
+ '<input type=“text” data-testid=“text-input” value=“Something”/>',
+ );
+ });
+ });
+});