feat(governance): render markdown in proposal view (#3632)

This commit is contained in:
Edd 2023-05-07 17:45:19 +01:00 committed by GitHub
parent 28bcb4ada1
commit bf6ab32230
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 98 additions and 31 deletions

View File

@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
cy.getByTestId('app-announcement').should('not.exist');
});
it('should show open or enacted proposals with proposal summary', function () {
it('should show open or enacted proposals without proposal summary', function () {
cy.get('body').then(($body) => {
if (!$body.find('[data-testid="proposals-list-item"]').length) {
cy.createMarket();
@ -43,12 +43,6 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-type').invoke('text').should('not.be.empty');
cy.getByTestId('proposal-description')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-details')
.invoke('text')
.should('not.be.empty');
cy.getByTestId('proposal-status')
.invoke('text')
.should('not.be.empty');

View File

@ -0,0 +1,5 @@
function ReactMarkdown({ children }) {
return <div>{children}</div>;
}
export default ReactMarkdown;

View File

@ -3,9 +3,10 @@ import { generateProposal } from '../../test-helpers/generate-proposals';
import { ProposalHeader } from './proposal-header';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
const renderComponent = (proposal: ProposalQuery['proposal']) => (
<ProposalHeader proposal={proposal} />
);
const renderComponent = (
proposal: ProposalQuery['proposal'],
isListItem = true
) => <ProposalHeader proposal={proposal} isListItem={isListItem} />;
describe('Proposal header', () => {
it('Renders New market proposal', () => {
@ -40,9 +41,6 @@ describe('Proposal header', () => {
'New some market'
);
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New market');
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
'A new some market'
);
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'tGBP settled future.'
);
@ -191,7 +189,7 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-details')).toHaveTextContent('short');
});
it('Renders Freeform proposal - long rationale (105 chars)', () => {
it('Renders Freeform proposal - long rationale (105 chars) - listing', () => {
render(
renderComponent(
generateProposal({
@ -209,16 +207,39 @@ describe('Proposal header', () => {
})
)
);
// For a rationale over 100 chars, we expect the header to be truncated at
// 100 chars with ellipsis and the details-one element to contain the rest.
expect(screen.getByTestId('proposal-title')).toHaveTextContent('0x0');
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
'Class aptent taciti sociosqu ad litora torquent per conubia'
);
// Rationale in list view is not rendered
expect(
screen.queryByTestId('proposal-description')
).not.toBeInTheDocument();
expect(screen.getByTestId('proposal-details')).toHaveTextContent('long');
});
it('Renders Freeform proposal - long rationale (105 chars) - details', () => {
render(
renderComponent(
generateProposal({
id: 'long',
rationale: {
title: '0x0',
description:
'Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Aenean dolor.',
},
terms: {
change: {
__typename: 'NewFreeform',
},
},
}),
false
)
);
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
/Class aptent/
);
});
// Remove once proposals have rationale and re-enable above tests
it('Renders Freeform proposal - id for title', () => {
render(
@ -262,9 +283,6 @@ describe('Proposal header', () => {
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
'Update asset'
);
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
'Update asset'
);
expect(screen.getByText('foo')).toBeInTheDocument();
});

View File

@ -5,13 +5,15 @@ import { Heading, SubHeading } from '../../../../components/heading';
import type { ReactNode } from 'react';
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
import ReactMarkdown from 'react-markdown';
import { truncateMiddle } from '../../../../lib/truncate-middle';
export const ProposalHeader = ({
proposal,
useSubHeading = true,
isListItem = true,
}: {
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
useSubHeading?: boolean;
isListItem?: boolean;
}) => {
const { t } = useTranslation();
const change = proposal?.terms.change;
@ -95,8 +97,8 @@ export const ProposalHeader = ({
proposalType = t('UpdateAsset');
details = (
<>
`${t('Update asset')}`;
<Lozenge>{change.assetId}</Lozenge>
<span>{t('Asset ID')}:</span>
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
</>
);
break;
@ -106,7 +108,7 @@ export const ProposalHeader = ({
return (
<div className="text-sm mb-2">
<div data-testid="proposal-title">
{useSubHeading ? (
{isListItem ? (
<header>
<SubHeading title={titleContent || t('Unknown proposal')} />
</header>
@ -121,9 +123,21 @@ export const ProposalHeader = ({
<Lozenge variant={Intent.None}>{proposalType}</Lozenge>
</div>
)}
{description && (
<div data-testid="proposal-description">{description}</div>
</div>
<div className="flex items-center gap-2">
{description && !isListItem && (
<div data-testid="proposal-description" className="mb-4">
<ReactMarkdown
className="react-markdown-container"
/* Prevents HTML embedded in the description from rendering */
skipHtml={true}
/* Stops users embedding images which could be used for tracking */
disallowedElements={['img']}
linkTarget="_blank"
>
{description}
</ReactMarkdown>
</div>
)}
</div>

View File

@ -74,7 +74,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
return (
<AsyncRenderer data={params} loading={loading} error={error}>
<section data-testid="proposal">
<ProposalHeader proposal={proposal} useSubHeading={false} />
<ProposalHeader proposal={proposal} isListItem={false} />
<div className="mb-10">
<ProposalChangeTable proposal={proposal} />
</div>

View File

@ -58,3 +58,39 @@
.validators-table .ag-theme-balham-dark *:hover {
cursor: pointer;
}
/* Styles required to (effectively) un-override the
* reset styles so that the Proposal description fields
* render as you'd expect them to.
*
* Notes:
* - image embeds are disabled, so no styles are required
* - strong may not be required
* - skipHTML is enabled, so no nested HTML will be rendered. Only
* . valid markdown
*/
.dark .react-markdown-container,
.dark .react-markdown-container li,
.dark .react-markdown-container p {
color: #fff;
}
.react-markdown-container strong {
font-weight: bold;
}
.react-markdown-container ol {
margin-left: 1em;
}
.react-markdown-container li {
margin-left: 1em;
padding-left: 1em;
}
.react-markdown-container ol li {
list-style: decimal;
}
.react-markdown-container ul li {
list-style: circle;
}