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 23 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 Show resolved Hide resolved
@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

@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 Show resolved Hide resolved
src/types.ts Outdated
type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value?: any;
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 revert making type to optional. That's probably why the build fails

src/types.ts Outdated
@@ -63,8 +63,9 @@ export interface Schema {
questions: Array<{
id: string;
label?: string;
value?:string;
type: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Copy link
Contributor

Choose a reason for hiding this comment

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

Rather than adding this as a comment, lets follow what has been done in the form engine and add this as a rule to the eslintrc file. See https://github.com/openmrs/openmrs-esm-form-engine-lib/blob/7f9a0813b1c69450e2cc0fcfa5222cd880b6033d/.eslintrc#L16

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, @NethmiRodrigo! Adding it directly to the .eslintrc file, like in the form engine, was a great tip—appreciate the help!

Copy link
Member

Choose a reason for hiding this comment

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

Actually, we would prefer that the no-explicit-any rule is enabled and disclaimed like this where it's unavoidable (although, for preference, use unknown rather than any).

Copy link
Member

Choose a reason for hiding this comment

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

FWIW, the only reason its currently disabled in other repos is because historically we didn't enforce it.

@NethmiRodrigo
Copy link
Contributor

Alrighty! Great work @Twiineenock! Could you update your branch and we'll merge this in!

@@ -14,6 +14,7 @@ module.exports = {
'lodash-es': 'lodash',
'^dexie$': '<rootDir>/node_modules/dexie',
'^react-i18next$': '<rootDir>/__mocks__/react-i18next.js',
'react-markdown': '<rootDir>/__mocks__/react-markdown.tsx',
Copy link
Member

Choose a reason for hiding this comment

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

I don't love this type of mocking. It means that the tests cannot actually test this component. Is there a reason we need to stub it out like this?

Copy link
Contributor

Choose a reason for hiding this comment

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

This is a very bad response from me to say this as the reason, but I did this following what we had done in form-engine, because jest threw an error.

<SelectItem text={questionType} value={questionType} key={key} />
))}
</Select>
{questionToEdit.questionOptions.rendering !== 'markdown' && (
Copy link
Member

Choose a reason for hiding this comment

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

This seems very hacky. It probably makes sense to have some abstraction for "should render required fields" rather than just hard-coding "markdown" here.

Copy link
Contributor

Choose a reason for hiding this comment

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

@ibacher I think it might be easier to add this into the refactor

Comment on lines 132 to 134
<p className={styles.questionLabel}>
{question.label ? question.label : <MarkdownWrapper markdown={question.value} />}
</p>
Copy link
Member

Choose a reason for hiding this comment

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

This is an even weirder set of assumptions here, i.e., that any question without a label must be rendered via markdown. It would be better to check that the value is intended to be rendered in markdown.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@ibacher , does this imply that question type value can have other renderingTypes apart from markdown?

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe in the future, probably. It would be better to check the rendering type === markdown as a condition.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated this!

@@ -69,6 +70,45 @@ const DraggableQuestion: React.FC<DraggableQuestionProps> = ({
}
}, [handleDuplicateQuestion, isDragging, question, pageIndex, sectionIndex]);

const MarkdownWrapper: React.FC<{ markdown: string | Array<string> }> = ({ markdown }) => {
Copy link
Member

Choose a reason for hiding this comment

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

Split this out as it's own component in it's own file.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated this!

@ibacher ibacher changed the title feat (O3-3946): Add support for markdown questions (feat) O3-3946: Add support for markdown questions Nov 1, 2024
<SelectItem text={fieldType} value={fieldType} key={key} />
))}
</Select>
{'label' in questionToEdit && (
Copy link
Contributor

Choose a reason for hiding this comment

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

Since every question has a label, although these fields are not required for markdown questions, they would get rendered, which doesn't happen in add-question-modal. We should change this condition to match the one we used in the add-question-modal
Screenshot 2024-11-18 at 11 39 14 AM
Screenshot 2024-11-18 at 11 41 04 AM

</Select>
</>
)}
{!questionToEdit.value &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we conditionally displaying the 'renderingType' field only when it has no value? If the intent is to prevent users from modifying the rendering type, we don't need to do that. Let's always display this field regardless.

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.

3 participants