Add support for markdown in helper texts (for effects/action/condition descriptions)

This commit is contained in:
Florian Rival
2019-12-25 23:44:41 +01:00
parent e29c2d033c
commit 7c57881359
26 changed files with 191 additions and 50 deletions

View File

@@ -48,9 +48,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsNetworkExtension(
.AddParameter("scenevar", _("Reponse scene variable"), "", true)
.SetParameterLongDescription(
_("The response of the server will be stored, as a string, in this "
"variable. If the server returns JSON, you may want to use the "
"variable. If the server returns *JSON*, you may want to use the "
"action \"Convert JSON to a scene variable\" afterwards, to "
"explore the results with a structure variable."))
"explore the results with a *structure variable*."))
.MarkAsComplex();
extension

View File

@@ -134,7 +134,7 @@ export default class BehaviorField extends React.Component<
<SemiControlledAutoComplete
margin={this.props.isInline ? 'none' : 'dense'}
floatingLabelText={this._description}
helperText={this._longDescription}
helperMarkdownText={this._longDescription}
fullWidth
errorText={
!this._behaviorNames.length

View File

@@ -25,7 +25,7 @@ export default class DefaultField extends React.Component<
commitOnBlur
value={this.props.value}
floatingLabelText={description}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
onChange={(text: string) => this.props.onChange(text)}

View File

@@ -52,7 +52,7 @@ export default class ExternalEventsField extends React.Component<
floatingLabelText={
parameterMetadata ? parameterMetadata.getDescription() : undefined
}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
fullWidth

View File

@@ -290,7 +290,7 @@ export default class ExpressionField extends React.Component<Props, State> {
margin={this.props.isInline ? 'none' : 'dense'}
value={value}
floatingLabelText={description}
helperText={longDescription}
helperMarkdownText={longDescription}
hintText={expressionType === 'string' ? '""' : undefined}
inputStyle={styles.input}
onChange={this._handleChange}

View File

@@ -113,7 +113,7 @@ export default class KeyField extends Component<ParameterFieldProps, {||}> {
floatingLabelText={
parameterMetadata ? parameterMetadata.getDescription() : undefined
}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
fullWidth

View File

@@ -27,7 +27,7 @@ export default class LayerField extends Component<ParameterFieldProps, {||}> {
floatingLabelText={
parameterMetadata ? parameterMetadata.getDescription() : undefined
}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
fullWidth

View File

@@ -19,7 +19,7 @@ export default class MouseField extends Component {
margin={this.props.isInline ? 'none' : 'dense'}
fullWidth
floatingLabelText={description}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
value={value}

View File

@@ -49,7 +49,7 @@ export default class ObjectField extends React.Component<
globalObjectsContainer={this.props.globalObjectsContainer}
objectsContainer={this.props.objectsContainer}
floatingLabelText={this._description}
helperText={this._longDescription}
helperMarkdownText={this._longDescription}
fullWidth
errorTextIfInvalid={
this._allowedObjectType ? (

View File

@@ -24,7 +24,7 @@ export default class OperatorField extends Component<ParameterFieldProps> {
margin={this.props.isInline ? 'none' : 'dense'}
fullWidth
floatingLabelText={description}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
value={this.props.value}

View File

@@ -24,7 +24,7 @@ export default class RelationalOperatorField extends Component<ParameterFieldPro
margin={this.props.isInline ? 'none' : 'dense'}
fullWidth
floatingLabelText={description}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
value={this.props.value}

View File

@@ -32,7 +32,7 @@ export default class SceneNameField extends Component<
floatingLabelText={
parameterMetadata ? parameterMetadata.getDescription() : undefined
}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
fullWidth

View File

@@ -39,7 +39,7 @@ export default class StringWithSelectorField extends Component<
floatingLabelText={
parameterMetadata ? parameterMetadata.getDescription() : undefined
}
helperText={
helperMarkdownText={
parameterMetadata ? parameterMetadata.getLongDescription() : undefined
}
fullWidth

View File

@@ -42,7 +42,7 @@ export default class VariableField extends Component<Props, {||}> {
<SemiControlledAutoComplete
margin={this.props.isInline ? 'none' : 'dense'}
floatingLabelText={description}
helperText={
helperMarkdownText={
parameterMetadata
? parameterMetadata.getLongDescription()
: undefined

View File

@@ -12,7 +12,7 @@ import LeftLoader from '../UI/LeftLoader';
import PlaceholderLoader from '../UI/PlaceholderLoader';
import PlaceholderError from '../UI/PlaceholderError';
import ThemeConsumer from '../UI/Theme/ThemeConsumer';
import ReactMarkdown from 'react-markdown';
import { MarkdownText } from '../UI/MarkdownText';
import Text from '../UI/Text';
type Props = {|
@@ -104,10 +104,10 @@ export default class ExtensionInstallDialog extends Component<Props, State> {
{!extensionHeader ? (
<Text>{extensionShortHeader.shortDescription}</Text>
) : (
<ReactMarkdown
escapeHtml
<MarkdownText
source={extensionHeader.description}
className={muiTheme.markdownRootClassName}
useParagraphs
/>
)}
{!extensionHeader && !error && <PlaceholderLoader />}

View File

@@ -8,7 +8,7 @@ import {
} from '../../Utils/GDevelopServices/Release';
import EmptyMessage from '../../UI/EmptyMessage';
import PlaceholderLoader from '../../UI/PlaceholderLoader';
import ReactMarkdown from 'react-markdown';
import { MarkdownText } from '../../UI/MarkdownText';
import { Column, Line } from '../../UI/Grid';
import Window from '../../Utils/Window';
import { hasBreakingChange } from '../../Utils/GDevelopServices/Release';
@@ -23,15 +23,6 @@ type Props = {|
currentReleaseName: string,
|};
const markdownCustomRenderers = {
// Ensure link are opened in a new page
link: props => (
<a href={props.href} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
),
};
/**
* Display information about latest releases.
*/
@@ -90,9 +81,8 @@ const ChangelogRenderer = ({ releases, error, currentReleaseName }: Props) => {
)}
{releases.map(release =>
release.name ? (
<ReactMarkdown
<MarkdownText
key={release.name}
escapeHtml
source={
'# Version ' +
release.name +
@@ -101,7 +91,7 @@ const ChangelogRenderer = ({ releases, error, currentReleaseName }: Props) => {
'Changes and new features description will be available soon.')
}
className={muiTheme.markdownRootClassName}
renderers={markdownCustomRenderers}
useParagraphs
/>
) : null
)}

View File

@@ -23,7 +23,7 @@ type Props = {|
fullWidth?: boolean,
floatingLabelText?: React.Node,
helperText?: React.Node,
helperMarkdownText?: ?string,
hintText?: MessageDescriptor | string,
openOnFocus?: boolean,
margin?: 'none' | 'dense',

View File

@@ -230,7 +230,7 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
id={field.name}
floatingLabelText={getFieldLabel(this.props.instances, field)}
floatingLabelFixed
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(this.props.instances, field)}
onChange={newValue => {
this.props.instances.forEach(i =>
setValue(i, parseFloat(newValue) || 0)
@@ -249,7 +249,7 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
key={field.name}
id={field.name}
floatingLabelText={getFieldLabel(this.props.instances, field)}
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(this.props.instances, field)}
disableAlpha
fullWidth
color={hexToRGBColor(getFieldValue(this.props.instances, field))}
@@ -274,7 +274,7 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
value={getFieldValue(this.props.instances, field)}
floatingLabelText={getFieldLabel(this.props.instances, field)}
floatingLabelFixed
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(this.props.instances, field)}
multiLine
style={styles.field}
/>
@@ -294,7 +294,10 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
id={field.name}
floatingLabelText={getFieldLabel(this.props.instances, field)}
floatingLabelFixed
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(
this.props.instances,
field
)}
onChange={newValue => {
this.props.instances.forEach(i => setValue(i, newValue || ''));
this._onInstancesModified(this.props.instances);
@@ -336,7 +339,7 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
value={getFieldValue(this.props.instances, field)}
key={field.name}
floatingLabelText={getFieldLabel(this.props.instances, field)}
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(this.props.instances, field)}
onChange={(event, index, newValue: string) => {
this.props.instances.forEach(i =>
setValue(i, parseFloat(newValue) || 0)
@@ -360,7 +363,7 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
)}
key={field.name}
floatingLabelText={getFieldLabel(this.props.instances, field)}
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(this.props.instances, field)}
onChange={(event, index, newValue: string) => {
this.props.instances.forEach(i => setValue(i, newValue || ''));
this._onInstancesModified(this.props.instances);
@@ -418,7 +421,7 @@ export default class PropertiesEditor extends React.Component<Props, {||}> {
this._onInstancesModified(this.props.instances);
}}
floatingLabelText={getFieldLabel(this.props.instances, field)}
helperText={getFieldDescription(this.props.instances, field)}
helperMarkdownText={getFieldDescription(this.props.instances, field)}
/>
);
};

View File

@@ -33,7 +33,7 @@ type Props = {|
initialResourceName: string,
onChange: string => void,
floatingLabelText?: React.Node,
helperText?: React.Node,
helperMarkdownText?: ?string,
hintText?: MessageDescriptor,
margin?: 'none' | 'dense',
|};
@@ -263,7 +263,7 @@ export default class ResourceSelector extends React.Component<Props, State> {
renderTextField={() => (
<SemiControlledAutoComplete
floatingLabelText={this.props.floatingLabelText}
helperText={this.props.helperText}
helperMarkdownText={this.props.helperMarkdownText}
hintText={this.props.hintText}
openOnFocus
dataSource={this.autoCompleteData || []}

View File

@@ -33,7 +33,7 @@ export default class ColorField extends Component {
fullWidth
floatingLabelText={this.props.floatingLabelText}
floatingLabelFixed
helperText={this.props.helperText}
helperMarkdownText={this.props.helperMarkdownText}
type="text"
hintText={t`Click to choose`}
onClick={this.onClick}

View File

@@ -0,0 +1,41 @@
// @flow
import * as React from 'react';
import ReactMarkdown from 'react-markdown';
// Sensible defaults for react-markdown
const makeMarkdownCustomRenderers = (useParagraphs: boolean) => ({
// Ensure link are opened in a new page
link: props => (
<a href={props.href} target="_blank" rel="noopener noreferrer">
{props.children}
</a>
),
// Add paragraphs only if we explictly opt in.
paragraph: props =>
useParagraphs ? <p>{props.children}</p> : props.children,
});
type Props = {|
source: string,
className?: ?string,
useParagraphs?: boolean,
|};
/**
* Display a markdown text
*/
export const MarkdownText = (props: Props) => {
const markdownCustomRenderers = React.useMemo(
() => makeMarkdownCustomRenderers(props.useParagraphs || false),
[props.useParagraphs]
);
return (
<ReactMarkdown
escapeHtml
source={props.source}
className={props.className}
renderers={markdownCustomRenderers}
/>
);
};

View File

@@ -5,6 +5,7 @@ import { I18n } from '@lingui/react';
import TextField from '@material-ui/core/TextField';
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import { computeTextFieldStyleProps } from './TextField';
import { MarkdownText } from './MarkdownText';
type ValueProps = {|
value: number | string,
@@ -31,7 +32,7 @@ type Props = {|
margin?: 'none' | 'dense',
floatingLabelText?: React.Node,
helperText?: React.Node,
helperMarkdownText?: ?string,
// If a hint text is specified, will be shown as an option for the empty
// value (""), disabled.
@@ -73,6 +74,10 @@ export default class SelectField extends React.Component<Props, {||}> {
}
const displayedValue = hasValidValue ? props.value : INVALID_VALUE;
const helperText = props.helperMarkdownText ? (
<MarkdownText source={props.helperMarkdownText} />
) : null;
return (
<I18n>
{({ i18n }) => (
@@ -82,7 +87,7 @@ export default class SelectField extends React.Component<Props, {||}> {
disabled={props.disabled}
fullWidth={props.fullWidth}
label={props.floatingLabelText}
helperText={props.helperText}
helperText={helperText}
value={displayedValue}
onChange={
onChange

View File

@@ -13,6 +13,7 @@ import muiZIndex from '@material-ui/core/styles/zIndex';
import SvgIcon from '@material-ui/core/SvgIcon';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import { computeTextFieldStyleProps } from './TextField';
import { MarkdownText } from './MarkdownText';
export type DataSource = Array<
| {|
@@ -124,7 +125,7 @@ type Props = {|
errorText?: React.Node,
disabled?: boolean,
floatingLabelText?: React.Node,
helperText?: React.Node,
helperMarkdownText?: ?string,
hintText?: MessageDescriptor | string,
fullWidth?: boolean,
margin?: 'none' | 'dense',
@@ -182,6 +183,10 @@ export default class SemiControlledAutoComplete extends React.Component<
const currentInputValue =
state.inputValue !== null ? state.inputValue : props.value;
const helperText = props.helperMarkdownText ? (
<MarkdownText source={props.helperMarkdownText} />
) : null;
return (
<I18n>
{({ i18n }) => (
@@ -285,7 +290,7 @@ export default class SemiControlledAutoComplete extends React.Component<
// Error handling:
error: !!props.errorText,
helperText: props.errorText || props.helperText,
helperText: props.errorText || helperText,
// Display:
InputLabelProps: getLabelProps({ shrink: true }),

View File

@@ -32,7 +32,7 @@ type Props = {|
floatingLabelText?: React.Node,
fullWidth?: boolean,
hintText?: React.Node,
helperText?: React.Node,
helperMarkdownText?: React.Node,
id?: string,
inputStyle?: Object,
max?: number,

View File

@@ -4,6 +4,7 @@ import ReactDOM from 'react-dom';
import { I18n } from '@lingui/react';
import MUITextField from '@material-ui/core/TextField';
import { type MessageDescriptor } from '../Utils/i18n/MessageDescriptor.flow';
import { MarkdownText } from './MarkdownText';
type ValueProps =
// Support "text" and "password" type:
@@ -55,7 +56,7 @@ type Props = {|
floatingLabelText?: React.Node,
name?: string,
hintText?: MessageDescriptor,
helperText?: React.Node,
helperMarkdownText?: ?string,
id?: string,
// Keyboard focus:
@@ -178,6 +179,10 @@ export default class TextField extends React.Component<Props, {||}> {
const { props } = this;
const onChange = props.onChange || undefined;
const helperText = props.helperMarkdownText ? (
<MarkdownText source={props.helperMarkdownText} />
) : null;
return (
<I18n>
{({ i18n }) => (
@@ -195,7 +200,7 @@ export default class TextField extends React.Component<Props, {||}> {
}
// Error handling:
error={!!props.errorText}
helperText={props.errorText || props.helperText}
helperText={props.errorText || helperText}
disabled={props.disabled}
InputLabelProps={{
shrink: props.floatingLabelFixed ? true : undefined,

View File

@@ -176,6 +176,8 @@ import {
ResponsiveLineStackLayout,
} from '../UI/Layout';
import { makeTestExtensions } from '../fixtures/TestExtensions';
import SelectField from '../UI/SelectField';
import SelectOption from '../UI/SelectOption';
// No i18n in this file
@@ -320,6 +322,62 @@ storiesOf('UI Building Blocks/Buttons', module)
</Column>
));
storiesOf('UI Building Blocks/SelectField', module)
.addDecorator(muiDecorator)
.add('default', () => (
<ValueStateHolder
initialValue={'1'}
render={(value, onChange) => (
<SelectField
value={value}
onChange={(e, i, newValue: string) => onChange(newValue)}
fullWidth
>
<SelectOption value="1" primaryText="Choice 1" />
<SelectOption value="2" primaryText="Choice 2" />
<SelectOption value="3" primaryText="Choice 3" />
<SelectOption value="4" primaryText="Choice 4" />
</SelectField>
)}
/>
))
.add('default, with (markdown) helper text', () => (
<ValueStateHolder
initialValue={'1'}
render={(value, onChange) => (
<SelectField
value={value}
onChange={(e, i, newValue: string) => onChange(newValue)}
fullWidth
helperMarkdownText="This is some help text that can be written in **markdown**. This is *very* useful for emphasis and can even be used to add [links](http://example.com)."
>
<SelectOption value="1" primaryText="Choice 1" />
<SelectOption value="2" primaryText="Choice 2" />
<SelectOption value="3" primaryText="Choice 3" />
<SelectOption value="4" primaryText="Choice 4" />
</SelectField>
)}
/>
))
.add('margin=none', () => (
<ValueStateHolder
initialValue={'1'}
render={(value, onChange) => (
<SelectField
margin="none"
value={value}
onChange={(e, i, newValue: string) => onChange(newValue)}
fullWidth
>
<SelectOption value="1" primaryText="Choice 1" />
<SelectOption value="2" primaryText="Choice 2" />
<SelectOption value="3" primaryText="Choice 3" />
<SelectOption value="4" primaryText="Choice 4" />
</SelectField>
)}
/>
));
storiesOf('UI Building Blocks/SemiControlledTextField', module)
.addDecorator(muiDecorator)
.add('default', () => (
@@ -399,6 +457,21 @@ storiesOf('UI Building Blocks/SemiControlledTextField', module)
</React.Fragment>
)}
/>
))
.add('with a (markdown) helper text', () => (
<ValueStateHolder
initialValue={'Hello World'}
render={(value, onChange) => (
<React.Fragment>
<SemiControlledTextField
helperMarkdownText="This is some help text that can be written in **markdown**. This is *very* useful for emphasis and can even be used to add [links](http://example.com)."
value={value}
onChange={onChange}
/>
<p>State value is {value}</p>
</React.Fragment>
)}
/>
));
storiesOf('UI Building Blocks/DragAndDrop', module).add('test bed', () => (
@@ -535,6 +608,25 @@ storiesOf('UI Building Blocks/SemiControlledAutoComplete', module)
</React.Fragment>
)}
/>
))
.add('with a (markdown) helper text', () => (
<ValueStateHolder
initialValue={'Choice 6'}
render={(value, onChange) => (
<React.Fragment>
<SemiControlledAutoComplete
value={value}
onChange={onChange}
helperMarkdownText="This is some help text that can be written in **markdown**. This is *very* useful for emphasis and can even be used to add [links](http://example.com)."
dataSource={[1, 2, 3, 4, 5, 6, 7, 8, 9].map(i => ({
text: `Choice ${i}`,
value: `Choice ${i}`,
}))}
/>
<p>State value is {value}</p>
</React.Fragment>
)}
/>
));
storiesOf('UI Building Blocks/SearchBar', module)
@@ -960,7 +1052,7 @@ storiesOf('UI Building Blocks/ColorField', module)
a: 255,
}}
onChangeComplete={() => {}}
helperText="Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
helperMarkdownText="Lorem ipsum **dolor sit amet**, consectetur _adipiscing elit_, [sed do eiusmod](http://example.com) tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat."
/>
<ColorField
floatingLabelText="This is not full width"