Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Markdown in descriptions #4405

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ should change the heading of the (upcoming) version to include a major version b
## @rjsf/core

- Fix default value population when switching between options in `MultiSchemaField` [#4375](https://github.com/rjsf-team/react-jsonschema-form/pull/4375). Fixes [#4367](https://github.com/rjsf-team/react-jsonschema-form/issues/4367)
- Updated `ObjectField` and `CheckboxWidget` to render markdown in the description when `enableMarkdownInDescription` is set to `true`, fixing [#3975](https://github.com/rjsf-team/react-jsonschema-form/issues/3975)

## @rjsf/validator-ajv8

- Fixed issue where `ui:title` in anyOf/oneOf is not shown in error messages. Fixes [#4368](https://github.com/rjsf-team/react-jsonschema-form/issues/4368)

## Dev / docs / playground

- Fixed typo in `package.json`

# 5.23.1

## @rjsf/chakra-ui
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"prepare": "husky install",
"format": "prettier --write .",
"format-check": "prettier --check .",
"bump-all-packages": "echo 'NOTE: Make sure to sanity check the playground locally before commiting changes' && npm update --save && npm install && npm run lint && npm run build && npm run test",
"bump-all-packages": "echo 'NOTE: Make sure to sanity check the playground locally before committing changes' && npm update --save && npm install && npm run lint && npm run build && npm run test",
"bump-peer-deps": "node scripts/bump-peer-deps.js",
"refresh-node-modules": "rimraf packages/*/node_modules && rimraf node_modules && npm install",
"commit-package-changes": "git add package-lock.json packages/*/package*.json && cross-env CI=skipPrecommit git commit -m 'updated package*.json after versioning' && git push",
Expand Down
7 changes: 6 additions & 1 deletion packages/core/src/components/fields/ObjectField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,11 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo

const templateTitle = uiOptions.title ?? schema.title ?? title ?? name;
const description = uiOptions.description ?? schema.description;
const richDescription = uiOptions.enableMarkdownInDescription ? (
<Markdown options={{ disableParsingRawHTML: true }}>{description || ''}</Markdown>
) : (
description
);
let orderedProperties: string[];
try {
const properties = Object.keys(schemaProperties);
Expand All @@ -281,7 +286,7 @@ class ObjectField<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends Fo
const templateProps = {
// getDisplayLabel() always returns false for object types, so just check the `uiOptions.label`
title: uiOptions.label === false ? '' : templateTitle,
description: uiOptions.label === false ? undefined : description,
description: uiOptions.label === false ? undefined : richDescription,
properties: orderedProperties.map((name) => {
const addedByAdditionalProperties = has(schema, [PROPERTIES_KEY, name, ADDITIONAL_PROPERTY_FLAG]);
const fieldUiSchema = addedByAdditionalProperties ? uiSchema.additionalProperties : uiSchema[name];
Expand Down
13 changes: 12 additions & 1 deletion packages/core/src/components/widgets/CheckboxWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
RJSFSchema,
StrictRJSFSchema,
WidgetProps,
getUiOptions,
} from '@rjsf/utils';
import Markdown from 'markdown-to-jsx';

/** The `CheckBoxWidget` is a widget for rendering boolean properties.
* It is typically used to represent a boolean.
Expand All @@ -32,6 +34,9 @@ function CheckboxWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
onChange,
registry,
}: WidgetProps<T, S, F>) {
const { globalUiOptions } = registry;
const uiOptions = getUiOptions<T, S, F>(uiSchema, globalUiOptions);

const DescriptionFieldTemplate = getTemplate<'DescriptionFieldTemplate', T, S, F>(
'DescriptionFieldTemplate',
registry,
Expand All @@ -58,12 +63,18 @@ function CheckboxWidget<T = any, S extends StrictRJSFSchema = RJSFSchema, F exte
);
const description = options.description ?? schema.description;

const richDescription = uiOptions.enableMarkdownInDescription ? (
<Markdown options={{ disableParsingRawHTML: true }}>{description || ''}</Markdown>
) : (
description || ''
);

Comment on lines +66 to +71
Copy link
Member

@heath-freenome heath-freenome Dec 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dejanbelusic after looking around it turns out that every theme's CheckboxWidget has a similar description rendering logic in it. We suggest creating a new component in the @rjsf/utils package that wraps this logic as follows and then updating every FieldDescriptionTemplate to use it:

export interface RichDescriptionProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any> {
  /** The description of the field being rendered */
  description: ReactNode;
  /** The uiSchema that was passed to field */
  uiSchema?: UiSchema<T, S, F>;
  /** The `registry` object */
  registry: Registry<T, S, F>;
}
export default function RichDescription<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>({ description, registry, uiSchema = {}}: RichDescriptionProps<T,S,F>) {
  const { globalUiOptions } = registry;
  const uiOptions = getUiOptions<T, S, F>(uiSchema, globalUiOptions);
  if (uiOptions.enableMarkdownInDescription) {
    return <Markdown options={{ disableParsingRawHTML: true }}>{description || ''}</Markdown>
 }
 return description;
}

With all the FieldDescriptionTemplates calling it similarly to the change that would be made in @rjsf/core's implementation:

export default function DescriptionField<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any
>(props: DescriptionFieldProps<T, S, F>) {
  const { id, description, uiSchema, registry } = props;
  if (!description) {
    return null;
  }
  return (
   <div id={id} className='field-description'>
      <RichDescription<T,S,F> description={description} uiSchema={uiSchema} registry={registry} />
    </div>
  );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @heath-freenome, I see your point and I agree. I can create a new component RichDescription as suggested, place it in the @rjsf/utils, and then use it in every DescriptionField component.

When looking into DescriptionField components, I can see that one in @rjsf/antd packages is wrapped in a span, the one in @rjsf/bootstrap-4 is wrapped in two divs, the one in @rjsf/chakra-ui package is wrapped in Text component... What I'm saying is every theme has similar, but different implementation. Do I leave them as is?

return (
<div className={`checkbox ${disabled || readonly ? 'disabled' : ''}`}>
{!hideLabel && !!description && (
<DescriptionFieldTemplate
id={descriptionId<T>(id)}
description={description}
description={richDescription}
schema={schema}
uiSchema={uiSchema}
registry={registry}
Expand Down
72 changes: 72 additions & 0 deletions packages/core/test/ObjectField.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -1203,4 +1203,76 @@ describe('ObjectField', () => {
});
});
});

describe('markdown', () => {
const schema = {
title: 'A list of tasks',
type: 'object',
properties: {
tasks: {
description: 'New *description*, with some Markdown.',
type: 'object',
title: 'Tasks',
properties: {
details: {
type: 'string',
title: 'Task details',
description: 'Description to renders Markdown **correctly**.',
},
has_markdown: {
type: 'boolean',
description: 'Checkbox with some `markdown`!',
},
},
},
},
};

const uiSchema = {
tasks: {
'ui:enableMarkdownInDescription': true,
details: {
'ui:enableMarkdownInDescription': true,
'ui:widget': 'textarea',
},
has_markdown: {
'ui:enableMarkdownInDescription': true,
},
},
};

it('should render markdown in description when enableMarkdownInDescription is set to true', () => {
const { node } = createFormComponent({ schema, uiSchema });

const { innerHTML: rootInnerHTML } = node.querySelector('form .form-group .form-group .field-description');
expect(rootInnerHTML).to.contains('New <em>description</em>, with some Markdown.');

const { innerHTML: detailsInnerHTML } = node.querySelector(
'form .form-group .form-group .field-string .field-description'
);
expect(detailsInnerHTML).to.contains('Description to renders Markdown <strong>correctly</strong>.');

const { innerHTML: checkboxInnerHTML } = node.querySelector(
'form .form-group .form-group .field-boolean .field-description'
);
expect(checkboxInnerHTML).to.contains('Checkbox with some <code>markdown</code>!');
});
it('should not render markdown in description when enableMarkdownInDescription is not present in uiSchema', () => {
const { node } = createFormComponent({ schema });

const { innerHTML: rootInnerHTML } = node.querySelector('form .form-group .form-group .field-description');
expect(rootInnerHTML).to.contains('New *description*, with some Markdown.');

const { innerHTML: detailsInnerHTML } = node.querySelector(
'form .form-group .form-group .field-string .field-description'
);
expect(detailsInnerHTML).to.contains('Description to renders Markdown **correctly**.');

const { innerHTML: checkboxInnerHTML } = node.querySelector(
'form .form-group .form-group .field-boolean .field-description'
);
expect(checkboxInnerHTML).to.contains('Checkbox with some `markdown`!');
});

});
});
2 changes: 1 addition & 1 deletion packages/utils/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,7 +683,7 @@ export type ObjectFieldTemplateProps<
/** A string value containing the title for the object */
title: string;
/** A string value containing the description for the object */
description?: string;
description?: ReactNode;
/** A boolean value stating if the object is disabled */
disabled?: boolean;
/** An array of objects representing the properties in the object */
Expand Down
Loading