From 075bce18b42af069d153bcaa6cbcb723a19964c6 Mon Sep 17 00:00:00 2001 From: Chris Thielen Date: Thu, 10 Oct 2019 14:04:54 -0700 Subject: [PATCH] feat(core/presentation): allow markdown in ValidationMessage --- .../core/src/presentation/Markdown.less | 9 +++++ .../core/src/presentation/Markdown.tsx | 36 +++++++++---------- .../forms/validation/ValidationMessage.tsx | 5 ++- .../forms/validation/categories.spec.ts | 6 ++++ .../forms/validation/categories.ts | 2 +- 5 files changed, 38 insertions(+), 20 deletions(-) create mode 100644 app/scripts/modules/core/src/presentation/Markdown.less diff --git a/app/scripts/modules/core/src/presentation/Markdown.less b/app/scripts/modules/core/src/presentation/Markdown.less new file mode 100644 index 00000000000..47c3d48e1ba --- /dev/null +++ b/app/scripts/modules/core/src/presentation/Markdown.less @@ -0,0 +1,9 @@ +.Markdown { + p { + margin: 0; + } + + p + p { + margin-top: 4px; + } +} diff --git a/app/scripts/modules/core/src/presentation/Markdown.tsx b/app/scripts/modules/core/src/presentation/Markdown.tsx index 84451a7fa55..5d2aebec8ee 100644 --- a/app/scripts/modules/core/src/presentation/Markdown.tsx +++ b/app/scripts/modules/core/src/presentation/Markdown.tsx @@ -1,40 +1,40 @@ import * as React from 'react'; import { HtmlRenderer, Parser } from 'commonmark'; import * as DOMPurify from 'dompurify'; +import './Markdown.less'; export interface IMarkdownProps { [key: string]: any; /** markdown */ message: string; + /** optional tag */ tag?: string; + + /** The className(s) to apply to the tag (.Markdown class is always applied) */ + className?: string; } /** * Renders markdown into a div (or some other tag) * Extra props are passed through to the rendered tag */ -export class Markdown extends React.Component { - public static defaultProps: Partial = { - tag: 'div', - }; - - private parser: Parser = new Parser(); - private renderer: HtmlRenderer = new HtmlRenderer(); +export function Markdown(props: IMarkdownProps) { + const { message, tag: tagProp, className: classNameProp, tag = 'div', ...rest } = props; - public render() { - const { message, tag, ...rest } = this.props; + const parser = React.useMemo(() => new Parser(), []); + const renderer = React.useMemo(() => new HtmlRenderer(), []); + const className = `Markdown ${classNameProp || ''}`; - if (message == null) { - return null; - } + if (message == null) { + return null; + } - const restProps = rest as React.DOMAttributes; - const parsed = this.parser.parse(message.toString()); - const rendered = this.renderer.render(parsed); - restProps.dangerouslySetInnerHTML = { __html: DOMPurify.sanitize(rendered) }; + const restProps = rest as React.DOMAttributes; + const parsed = parser.parse(message.toString()); + const rendered = renderer.render(parsed); + restProps.dangerouslySetInnerHTML = { __html: DOMPurify.sanitize(rendered) }; - return React.createElement(tag, restProps); - } + return React.createElement(tag, { ...restProps, className }); } diff --git a/app/scripts/modules/core/src/presentation/forms/validation/ValidationMessage.tsx b/app/scripts/modules/core/src/presentation/forms/validation/ValidationMessage.tsx index 69cdbaa1f1c..f08f1cb0001 100644 --- a/app/scripts/modules/core/src/presentation/forms/validation/ValidationMessage.tsx +++ b/app/scripts/modules/core/src/presentation/forms/validation/ValidationMessage.tsx @@ -1,4 +1,7 @@ import * as React from 'react'; +import { isString } from 'lodash'; + +import { Markdown } from '../../Markdown'; import { ICategorizedErrors, IValidationCategory } from './categories'; import './ValidationMessage.less'; @@ -43,7 +46,7 @@ export const ValidationMessage = (props: IValidationMessageProps) => { return (
{showIcon && } -
{message}
+
{isString(message) ? : message}
); }; diff --git a/app/scripts/modules/core/src/presentation/forms/validation/categories.spec.ts b/app/scripts/modules/core/src/presentation/forms/validation/categories.spec.ts index 44dd69eed5d..1ea56d7f74e 100644 --- a/app/scripts/modules/core/src/presentation/forms/validation/categories.spec.ts +++ b/app/scripts/modules/core/src/presentation/forms/validation/categories.spec.ts @@ -34,6 +34,12 @@ describe('categorizeErrorMessage', () => { it('returns the error message without the label prefix', () => { expect(categorizeValidationMessage('Warning: something sorta bad')[1]).toEqual('something sorta bad'); }); + + it('supports newlines embedded in the message', () => { + const [status, message] = categorizeValidationMessage('Warning: something sorta bad\n\nhappened'); + expect(status).toBe('warning'); + expect(message).toBe('something sorta bad\n\nhappened'); + }); }); describe('category message builder', () => { diff --git a/app/scripts/modules/core/src/presentation/forms/validation/categories.ts b/app/scripts/modules/core/src/presentation/forms/validation/categories.ts index 0a9e884ad4a..be4c5b6db50 100644 --- a/app/scripts/modules/core/src/presentation/forms/validation/categories.ts +++ b/app/scripts/modules/core/src/presentation/forms/validation/categories.ts @@ -38,7 +38,7 @@ export const warningMessage = buildCategoryMessage('warning'); // A regular expression which captures the category label and validation message from a validation message // I.e., for the string: "Error: There was a fatal error" // this captures "Error" and "There was a fatal error" -const validationMessageRegexp = new RegExp(`^(${labels.join('|')}): (.*)$`); +const validationMessageRegexp = new RegExp(`^(${labels.join('|')}): (.*)$`, 'sm'); // Takes an errorMessage with embedded category and extracts the category and message // Example: "Error: there was an error" => ['error', 'there was an error']