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”/>', + ); + }); + }); +});