Skip to content
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
32 changes: 32 additions & 0 deletions cypress/e2e/Markdown/Markdown.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
Feature: Feedback widget renders markdown content from program rules

Scenario: Feedback widget is visible on the enrollment dashboard
Given user lands on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=EKlWxFF9NPb
Then the feedback widget should be visible

Scenario: Feedback widget list contains 4 items
Given user lands on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=EKlWxFF9NPb
Then the list should contain 4 items

Scenario: First feedback item renders headers markdown correctly
Given user lands on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=EKlWxFF9NPb
Then the first list item should contain p with text "Headers"
And the first list item should contain h1 with text "Header 1"
And the first list item should contain h2 with text "Header 2"
And the first list item should contain h3 with text "Header 3"
And the first list item should contain h4 with text "Header 4"
And the first list item should contain h5 with text "Header 5"
And the first list item should contain h6 with text "Header 6"

Scenario: Second feedback item renders italic heading and table correctly
Given user lands on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=EKlWxFF9NPb
Then the second list item h6 should contain italic text "Feedback"
And the second list item table should match:
| Column A | Column B |
| Content A | Content B |

Scenario: Fourth feedback item renders score with color indicator correctly
Given user lands on the enrollment edit event page by having typed #/enrollmentEventEdit?eventId=EKlWxFF9NPb
Then the fourth list item left section should have h1 with text "1. Score 1"
And the fourth list item right section color indicator should be "rgb(57, 166, 45)"
And the fourth list item right section should have p with text "90%"
79 changes: 79 additions & 0 deletions cypress/e2e/Markdown/Markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import {Given, Then, defineStep as And} from '@badeball/cypress-cucumber-preprocessor';

const LONG_TIMEOUT = { timeout: 120000 };

const getFeedbackDiv = () =>
cy.get('[data-test="feedback-widget"]', LONG_TIMEOUT);

// SHARED

Given(/^user lands on the enrollment edit event page by having typed (.*)$/, (url) => {
cy.visit(url, LONG_TIMEOUT);
getFeedbackDiv().should('exist');
});

Then('the feedback widget should be visible', () => {
getFeedbackDiv().should('be.visible');
});

Then('the list should contain {int} items', (expectedCount) => {
getFeedbackDiv()
.find('[data-test="widget-contents"]', LONG_TIMEOUT)
.find('ul')
.find('li')
.should('have.length', expectedCount);
});

const getListItem = (n) =>
getFeedbackDiv()
.find('[data-test="widget-contents"]', LONG_TIMEOUT)
.find('ul li')
.eq(n - 1);

// FIRST FEEDBACK ELEMENT

And('the first list item should contain {word} with text {string}', (tag, text) => {
getListItem(1)
.find(tag)
.should('have.text', text);
});

// SECOND FEEDBACK ELEMENT

Then('the second list item {word} should contain italic text {string}', (tag, text) => {
getListItem(2)
.find(tag)
.find('em')
.should('have.text', text);
});

And('the second list item table should match:', (dataTable) => {
const [headerRow, ...bodyRows] = dataTable.raw();

getListItem(2).find('table').within(() => {
cy.get('thead th').each(($th, i) => {
expect($th.text()).to.equal(headerRow[i]);
});

cy.get('tbody tr').each(($tr, rowIndex) => {
bodyRows[rowIndex].forEach((cell, colIndex) => {
expect($tr.find('td').eq(colIndex).text()).to.equal(cell);
});
});
});
});


// FOURTH FEEDBACK ELEMENT

Then('the fourth list item left section should have {word} with text {string}', (tag, text) => {
getListItem(4).find('> div > div').first().find(tag).should('have.text', text);
});

And('the fourth list item right section color indicator should be {string}', (color) => {
getListItem(4).find('> div > div').last().find('span').should('have.css', 'background-color', color);
});

And('the fourth list item right section should have {word} with text {string}', (tag, text) => {
getListItem(4).find('> div > div').last().find(tag).should('have.text', text);
});
4 changes: 2 additions & 2 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ msgstr ""
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
"POT-Creation-Date: 2026-05-26T07:35:44.230Z\n"
"PO-Revision-Date: 2026-05-26T07:35:44.230Z\n"
"POT-Creation-Date: 2026-06-17T22:59:57.403Z\n"
"PO-Revision-Date: 2026-06-17T22:59:57.404Z\n"

msgid "The application could not be loaded."
msgstr "The application could not be loaded."
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"react-leaflet": "^2.8.0",
"react-leaflet-draw": "0.19.0",
"react-leaflet-search-unpolyfilled": "^0.1.0",
"react-markdown": "^10.1.0",
"react-popper": "^2.2.4",
"react-redux": "^9.2.0",
"react-router-dom": "^5.3.0",
Expand All @@ -57,6 +58,7 @@
"redux-logger": "^3.0.6",
"redux-observable": "^2.0.0",
"regenerator-runtime": "^0.14.1",
"remark-gfm": "^4.0.1",
"reselect": "^4.1.7",
"rxjs": "^7.8.2",
"typeface-roboto": "^1.1.13",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ export type FilteredFeedbackText = {
id: string;
message: string;
color?: string;
priority?: number;
legendSetId?: string;
}

export type FilteredFeedbackKeyValue = {
id: string;
key: string;
value: string;
color?: string;
priority?: number;
legendSetId?: string;
}

export type FeedbackWidgetData = string | FilteredFeedbackText | FilteredFeedbackKeyValue;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
import React, { ComponentType, memo } from 'react';
import { spacersNum, colors } from '@dhis2/ui';
import Markdown from 'react-markdown';
import { withStyles, type WithStyles } from 'capture-core-utils/styles';
import remarkGfm from 'remark-gfm';
import { MarkdownWrapperProps } from './MarkdownWrapper.types';


const styles = {
container: {
width: '100%',
margin: '8px 0px',
display: 'flex',
gap: '1rem',
},
title: {
flex: '1 0 50%',
minWidth: '50%',
},
content: {
flex: '0 1 auto',
maxWidth: '50%',
minWidth: '0',
display: 'flex',
alignItems: 'center',
gap: '8px',
},
legend: {
height: '12px',
width: '12px',
minWidth: '12px',
borderRadius: '2px',
},
markdown: {
fontSize: '14px',
lineHeight: 1.45,
color: colors.grey900,
'&:first-child': {
marginTop: 0,
},
'& p': {
margin: `${spacersNum.dp8}px 0 0 0`,
display: 'inline-block',
},
'& p:first-child': {
marginTop: 0,
},
'& h1': {
fontSize: '18px',
fontWeight: 700,
lineHeight: 1.25,
margin: `${spacersNum.dp12}px 0 ${spacersNum.dp8}px 0`,
paddingBottom: spacersNum.dp4,
borderBottom: `2px solid ${colors.grey400}`,
color: colors.grey900,
},
'& h2': {
fontSize: '16px',
fontWeight: 700,
margin: `${spacersNum.dp12}px 0 ${spacersNum.dp4}px 0`,
color: colors.grey900,
},
'& h3': {
fontSize: '15px',
fontWeight: 600,
margin: `${spacersNum.dp8}px 0 ${spacersNum.dp4}px 0`,
color: colors.grey900,
},
'& h4, & h5, & h6': {
fontSize: '14px',
fontWeight: 600,
margin: `${spacersNum.dp8}px 0 ${spacersNum.dp4}px 0`,
color: colors.grey900,
},
'& strong': {
fontWeight: 700,
color: colors.grey900,
},
'& em': {
fontStyle: 'italic',
color: colors.grey900,
},
'& a': {
color: colors.blue800,
textDecoration: 'underline',
},
'& ul, & ol': {
margin: `${spacersNum.dp8}px 0 0 0`,
paddingInlineStart: spacersNum.dp16,
},
'& li': {
marginTop: spacersNum.dp4,
},
'& code': {
fontFamily: 'Menlo, Monaco, Consolas, monospace',
fontSize: '12px',
background: colors.grey200,
padding: `1px ${spacersNum.dp4}px`,
borderRadius: '3px',
},
'& pre': {
margin: `${spacersNum.dp8}px 0 0 0`,
padding: spacersNum.dp8,
background: colors.grey200,
borderRadius: '4px',
overflow: 'auto',
fontSize: '12px',
},
'& pre code': {
background: 'transparent',
padding: 0,
},
'& blockquote': {
margin: `${spacersNum.dp8}px 0 0 0`,
paddingInlineStart: spacersNum.dp12,
borderInlineStart: `4px solid ${colors.grey400}`,
color: colors.grey800,
fontStyle: 'italic',
},
'& hr': {
margin: `${spacersNum.dp12}px 0`,
border: 'none',
borderTop: `1px solid ${colors.grey400}`,
},
'& table': {
borderCollapse: 'collapse',
width: '100%',
margin: `${spacersNum.dp8}px 0`,
fontSize: '13px',
lineHeight: '18px',
},
'& th, & td': {
padding: `${spacersNum.dp4}px ${spacersNum.dp8}px`,
border: `1px solid ${colors.grey300}`,
textAlign: 'left',
},
'& th': {
fontWeight: 600,
color: colors.grey800,
whiteSpace: 'nowrap',
},
'& td': {
color: colors.grey900,
},
},
};
const REMARK_PLUGINS = [remarkGfm];

type Props = MarkdownWrapperProps & WithStyles<typeof styles>;
const MarkdownWrapperComponent = memo(({ title, content, color, classes }:Props) => (
<div className={classes.container} >
<div className={classes.title}>
<div className={classes.markdown}>
<Markdown remarkPlugins={REMARK_PLUGINS}>{title}</Markdown>
</div>
</div>

{ content &&
<div className={classes.content}>
{ color && <span className={classes.legend} style={{ backgroundColor: color }} /> }
<div className={classes.markdown}>
<Markdown remarkPlugins={REMARK_PLUGINS}>{content}</Markdown>
</div>
</div>
}
</div>
));

export const MarkdownWrapper = withStyles(styles)(MarkdownWrapperComponent) as ComponentType<MarkdownWrapperProps>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export type MarkdownWrapperProps = {
title: string;
content?: string;
color?: string;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MarkdownWrapper } from './MarkdownWrapper';
Loading