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

feat (O3-3946): Add support for markdown questions #355

Open
wants to merge 16 commits into
base: main
Choose a base branch
from

Conversation

Twiineenock
Copy link
Contributor

@Twiineenock Twiineenock commented Oct 14, 2024

Requirements

  • This PR has a title that briefly describes the work done including the ticket number. If there is a ticket, make sure your PR title includes a conventional commit label. See existing PR titles for inspiration.
  • My work conforms to the OpenMRS 3.0 Styleguide and design documentation.
  • My work includes tests or is validated by existing tests.

Summary

Description

This pull request introduces support for creating markdown-based questions within the interactive form builder. Markdown questions are designed for scenarios where additional explanatory content is needed within the form, apart from standard question labels. This content can help users understand how to fill out fields, explain the meaning of specific fields, or add other context. Since these questions are meant for accessibility and enhanced guidance, they do not contain fields to fill out.

Note: This PR complements the work in the form-engine repository. See feat: Tweak the markdown support #411 for additional details on markdown support improvements related to this update.

Implementation Details

Currently, there is markdown rendering support in the openmrs-esm-form-engine-lib using react-markdown, which renders markdown strings as HTML elements within the UI.

To provide full markdown support with editing capabilities, this PR includes the following key components:

  • Markdown Editor UI: Integrated using react-mde, allowing users to easily create and edit markdown content.
  • Markdown Renderer: Uses react-markdown to safely render markdown content to React elements, supporting common markdown features such as:
    • Headings
    • Bold and Italics
    • Lists (ordered, unordered, and task lists)
    • Strikethrough
    • Superscript and Subscript

Key Changes

  1. UI Enhancements:

    • Added a markdown editor UI in the form builder, allowing users to create and edit markdown-based questions directly.
    • Ensured a rich text editing experience with full markdown support for common styling and structuring elements.
  2. Integration with form-engine:

    • Minor adjustments within the openmrs-esm-form-engine-lib to seamlessly integrate the markdown editor with existing question types.
    • Leveraged react-markdown within the form engine to convert markdown text into React-rendered HTML elements.

Features Added in this PR

  • Creation of New Markdown Questions: Users can now create new markdown-based questions, formatted using markdown syntax.
  • Editing Existing Markdown Questions: Markdown questions can be easily edited, providing flexibility in adding or updating content.

By implementing this feature, the form builder now offers greater flexibility and accessibility, allowing clients to incorporate rich text explanations and formatting within their forms.

Technical Note: The core of this markdown support relies on react-markdown for rendering and react-mde for user editing. These libraries handle markdown safely and ensure React compatibility.

Links

Screenshots

form-preview

interactive-builder

preview

Screancasts

Creating a new question:

creat.webm

Edit an existing Question

edit-qn.webm

Related Issue

https://openmrs.atlassian.net/browse/O3-3946

Other

@NethmiRodrigo
Copy link
Contributor

@Twiineenock could you please update your branch?

Comment on lines 107 to 109
<p className={styles.questionLabel}>
<MarkdownWrapper markdown={question.label || question.value} />
</p>
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the text shown in markdown can be quite long, unlike a question label, I don't think we should show the entirety of it here. Maybe we could just say Markdown content, or something along those lines?
@denniskigen thoughts?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@NethmiRodrigo , we have come up with this:

short-mark-down

If the markdown is very long, we show a small part of the markdown plus ellipsis to show that there is more content.

Copy link
Contributor

Choose a reason for hiding this comment

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

Lovely! Thanks @Twiineenock!

@@ -0,0 +1,54 @@
import React from 'react';
Copy link
Contributor

Choose a reason for hiding this comment

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

Love that you made this into a component of its own! Thank you!

Comment on lines 378 to 385
<Select
defaultValue={questionToEdit.questionOptions.rendering}
onChange={(event: React.ChangeEvent<HTMLSelectElement>) => setFieldType(event.target.value as RenderType)}
id="renderingType"
invalidText={t('validFieldTypeRequired', 'A valid field type value is required')}
labelText={t('renderingType', 'Rendering type')}
required
>
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd prefer to have the question label be the first input in the modal, it feels more intuitive.

import styles from './markdown-question.scss';

interface MarkdownQuestionProps {
palceHolder?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
palceHolder?: string;
placeholder?: string;

package.json Outdated
Comment on lines 59 to 60
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
Copy link
Contributor

@NethmiRodrigo NethmiRodrigo Oct 14, 2024

Choose a reason for hiding this comment

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

Are these libraries to support for html tags? If so, I'd rather we leave them out and not have support for HTML tags

Copy link
Contributor

@NethmiRodrigo NethmiRodrigo Oct 14, 2024

Choose a reason for hiding this comment

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

Because I'm concerned about XSS - https://www.npmjs.com/package/react-mde#xss-concerns
CCing @denniskigen

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

@Twiineenock Twiineenock Oct 14, 2024

Choose a reason for hiding this comment

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

Are these libraries to support for html tags? If so, I'd rather we leave them out and not have support for HTML tags

Thanks @NethmiRodrigo for the Review,

We do need remark-gfm if we are to have GitHub Flavored Markdown (GFM) e.g tables, task lists, strikethroughs, Autolinks e.t.c

And we need The rehypeRaw plugin processes raw HTML embedded within Markdown content, allowing it to render as HTML elements in the final output.

Are there some of these formatting options we don't need so we can keep it as simple as possible?

Copy link
Contributor

Choose a reason for hiding this comment

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

Lets limit the formatting options to only the ones that are currently supported by the react form engine. We don't need these libraries then right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lets limit the formatting options to only the ones that are currently supported by the react form engine. We don't need these libraries then right?

We now have this implemented

@Twiineenock
Copy link
Contributor Author

Hi @NethmiRodrigo, @denniskigen,

I'm currently trying to import the react-mde styles directly from the library instead of rewriting them. However, I noticed that our Webpack configuration is enabling CSS Modules globally, which is likely scoping the CSS classes to components, making them unavailable for global styles like those from react-mde.

Here’s the relevant part of the Webpack configuration:

const cssLoader = {
    loader: require.resolve('css-loader'),
    options: {
      modules: {
        localIdentName: `${ident}__[name]__[local]___[hash:base64:5]`,
      },
    },
  };

I suspect that this configuration is preventing the react-mde styles from being applied globally. Could this be the reason why the styles from react-mde aren't working as expected?

How can we tweak the Webpack config to ensure that CSS Modules are applied to our local styles, but exclude the styles from react-mde (or any other library in node_modules) so that they are applied globally without scoping?

Thanks!

@NethmiRodrigo
Copy link
Contributor

I can't seem to figure out why the e2e tests are failing, they pass perfectly fine locally. When creating a question, the save button in the modal stays disabled for some reason. Pinging @denniskigen for help

@NethmiRodrigo
Copy link
Contributor

NethmiRodrigo commented Oct 22, 2024

Hi @NethmiRodrigo, @denniskigen,

I'm currently trying to import the react-mde styles directly from the library instead of rewriting them. However, I noticed that our Webpack configuration is enabling CSS Modules globally, which is likely scoping the CSS classes to components, making them unavailable for global styles like those from react-mde.

Here’s the relevant part of the Webpack configuration:

const cssLoader = {
    loader: require.resolve('css-loader'),
    options: {
      modules: {
        localIdentName: `${ident}__[name]__[local]___[hash:base64:5]`,
      },
    },
  };

I suspect that this configuration is preventing the react-mde styles from being applied globally. Could this be the reason why the styles from react-mde aren't working as expected?

How can we tweak the Webpack config to ensure that CSS Modules are applied to our local styles, but exclude the styles from react-mde (or any other library in node_modules) so that they are applied globally without scoping?

Thanks!

@Twiineenock about this, with help from @ibacher, I changed the webpack config to overwrite the css rule, which is basically
config.cssRuleConfig.rules = [ { test: /\.css$/, use: [require.resolve('style-loader'), require.resolve('css-loader')], }, ];
And then in the markdown-question.component.tsx I imported the css file like so
import 'react-mde/lib/styles/css/react-mde-all.css';
The issue now is that we get an error saying Unknown word... which I saw on a stack overflow could be because the css loader and style loader libraries gets imported more than once

@NethmiRodrigo
Copy link
Contributor

@Twiineenock lets opt to leave the styling as you've done. Although for future work, we should make the markdown editor match the carbon design system - https://carbondesignsystem.com/patterns/text-toolbar-pattern/. All that's left is figuring out why the e2e test isn't working. I doubt it has anything to do with your changes because they pass locally

@Twiineenock
Copy link
Contributor Author

Twiineenock commented Oct 24, 2024

@Twiineenock lets opt to leave the styling as you've done. Although for future work, we should make the markdown editor match the carbon design system - https://carbondesignsystem.com/patterns/text-toolbar-pattern/. All that's left is figuring out why the e2e test isn't working. I doubt it has anything to do with your changes because they pass locally

Woow, great Idea @NethmiRodrigo ,
Could I apply carbon design system to the markdown editor before we could let this in?

Is it better when its a separate work?

@NethmiRodrigo NethmiRodrigo mentioned this pull request Oct 25, 2024
3 tasks
@NethmiRodrigo
Copy link
Contributor

NethmiRodrigo commented Oct 25, 2024

@Twiineenock if you would like to do it in this PR, we can go with that approach. I would prefer we get this in and do that work incrementally, since this feature works perfectly fine as is. About the e2e test, could you please update the save button in the add question and edit question modal to remove the check for the label to be not null for the button to be enabled, since we made it optional? I believe that is what could be why the e2e test fails

@Twiineenock
Copy link
Contributor Author

@Twiineenock if you would like to do it in this PR, we can go with that approach. I would prefer we get this in and do that work incrementally, since this feature works perfectly fine as is. About the e2e test, could you please update the save button in the add question and edit question modal to remove the check for the label to be not null for the button to be enabled, since we made it optional? I believe that is what could be why the e2e test fails

Alright we shall do that incrementally

@@ -322,14 +325,16 @@ const AddQuestionModal: React.FC<AddQuestionModalProps> = ({
<ModalBody hasScrollingContent>
<FormGroup legendText={''}>
<Stack gap={5}>
<TextInput
{renderingType === 'markdown' ? <MarkdownQuestion onValueChange={setQuestionValue}/> : (
<TextInput
id="questionLabel"
labelText={<RequiredLabel isRequired={isQuestionRequired} text={t('questionLabel', 'Label')} t={t} />}
placeholder={t('labelPlaceholder', 'e.g. Type of Anaesthesia')}
value={questionLabel}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setQuestionLabel(event.target.value)}
required
Copy link
Contributor

Choose a reason for hiding this comment

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

Shall we remove the required attributes and change the label to not be required as well, since it is no longer so

id={questionToEdit.id}
labelText={t('questionLabel', 'Label')}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => setQuestionLabel(event.target.value)}
required
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto

@NethmiRodrigo
Copy link
Contributor

Just a few final changes and we can merge this in @Twiineenock, could you also please update your branch?

src/types.ts Outdated
@@ -117,7 +117,7 @@ export interface Section {
export interface Question {
id: string;
label?: string;
value?:string;
value?: string;
type: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

We should make this optional since markdown content doesn't have a type

Copy link
Contributor

Choose a reason for hiding this comment

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

And we should update this in form engine too

Copy link
Contributor Author

Choose a reason for hiding this comment

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

src/types.ts Outdated
@@ -117,8 +117,8 @@ export interface Section {
export interface Question {
id: string;
label?: string;
value?:string;
type: string;
value?: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

@Twiineenock I see value is of type any in form-engine and not string

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@NethmiRodrigo I had set the type to string because of this eslint error Unexpected any. Specify a different type.eslint[@typescript-eslint/no-explicit-any](https://typescript-eslint.io/rules/no-explicit-any)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Should we go for any or string?

Copy link
Contributor Author

@Twiineenock Twiineenock Oct 25, 2024

Choose a reason for hiding this comment

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

No worries! Added this // eslint-disable-next-line @typescript-eslint/no-explicit-any inline comment to escape eslint check for type any when validating question value attribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants