feat(governance): render markdown in proposal view (#3632)
This commit is contained in:
parent
28bcb4ada1
commit
bf6ab32230
@ -27,7 +27,7 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
|||||||
cy.getByTestId('app-announcement').should('not.exist');
|
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) => {
|
cy.get('body').then(($body) => {
|
||||||
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
if (!$body.find('[data-testid="proposals-list-item"]').length) {
|
||||||
cy.createMarket();
|
cy.createMarket();
|
||||||
@ -43,12 +43,6 @@ context('Home Page - verify elements on page', { tags: '@smoke' }, function () {
|
|||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('not.be.empty');
|
.should('not.be.empty');
|
||||||
cy.getByTestId('proposal-type').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')
|
cy.getByTestId('proposal-status')
|
||||||
.invoke('text')
|
.invoke('text')
|
||||||
.should('not.be.empty');
|
.should('not.be.empty');
|
||||||
|
5
apps/governance/src/__mocks__/react-markdown.js
vendored
Normal file
5
apps/governance/src/__mocks__/react-markdown.js
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
function ReactMarkdown({ children }) {
|
||||||
|
return <div>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ReactMarkdown;
|
@ -3,9 +3,10 @@ import { generateProposal } from '../../test-helpers/generate-proposals';
|
|||||||
import { ProposalHeader } from './proposal-header';
|
import { ProposalHeader } from './proposal-header';
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
|
|
||||||
const renderComponent = (proposal: ProposalQuery['proposal']) => (
|
const renderComponent = (
|
||||||
<ProposalHeader proposal={proposal} />
|
proposal: ProposalQuery['proposal'],
|
||||||
);
|
isListItem = true
|
||||||
|
) => <ProposalHeader proposal={proposal} isListItem={isListItem} />;
|
||||||
|
|
||||||
describe('Proposal header', () => {
|
describe('Proposal header', () => {
|
||||||
it('Renders New market proposal', () => {
|
it('Renders New market proposal', () => {
|
||||||
@ -40,9 +41,6 @@ describe('Proposal header', () => {
|
|||||||
'New some market'
|
'New some market'
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('New 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(
|
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
||||||
'tGBP settled future.'
|
'tGBP settled future.'
|
||||||
);
|
);
|
||||||
@ -191,7 +189,7 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent('short');
|
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(
|
render(
|
||||||
renderComponent(
|
renderComponent(
|
||||||
generateProposal({
|
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-title')).toHaveTextContent('0x0');
|
||||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent('Freeform');
|
||||||
expect(screen.getByTestId('proposal-description')).toHaveTextContent(
|
// Rationale in list view is not rendered
|
||||||
'Class aptent taciti sociosqu ad litora torquent per conubia'
|
expect(
|
||||||
);
|
screen.queryByTestId('proposal-description')
|
||||||
|
).not.toBeInTheDocument();
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent('long');
|
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
|
// Remove once proposals have rationale and re-enable above tests
|
||||||
it('Renders Freeform proposal - id for title', () => {
|
it('Renders Freeform proposal - id for title', () => {
|
||||||
render(
|
render(
|
||||||
@ -262,9 +283,6 @@ describe('Proposal header', () => {
|
|||||||
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
expect(screen.getByTestId('proposal-type')).toHaveTextContent(
|
||||||
'Update asset'
|
'Update asset'
|
||||||
);
|
);
|
||||||
expect(screen.getByTestId('proposal-details')).toHaveTextContent(
|
|
||||||
'Update asset'
|
|
||||||
);
|
|
||||||
expect(screen.getByText('foo')).toBeInTheDocument();
|
expect(screen.getByText('foo')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -5,13 +5,15 @@ import { Heading, SubHeading } from '../../../../components/heading';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
import type { ProposalFieldsFragment } from '../../proposals/__generated__/Proposals';
|
||||||
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
import type { ProposalQuery } from '../../proposal/__generated__/Proposal';
|
||||||
|
import ReactMarkdown from 'react-markdown';
|
||||||
|
import { truncateMiddle } from '../../../../lib/truncate-middle';
|
||||||
|
|
||||||
export const ProposalHeader = ({
|
export const ProposalHeader = ({
|
||||||
proposal,
|
proposal,
|
||||||
useSubHeading = true,
|
isListItem = true,
|
||||||
}: {
|
}: {
|
||||||
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
proposal: ProposalFieldsFragment | ProposalQuery['proposal'];
|
||||||
useSubHeading?: boolean;
|
isListItem?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const change = proposal?.terms.change;
|
const change = proposal?.terms.change;
|
||||||
@ -95,8 +97,8 @@ export const ProposalHeader = ({
|
|||||||
proposalType = t('UpdateAsset');
|
proposalType = t('UpdateAsset');
|
||||||
details = (
|
details = (
|
||||||
<>
|
<>
|
||||||
`${t('Update asset')}`;
|
<span>{t('Asset ID')}:</span>
|
||||||
<Lozenge>{change.assetId}</Lozenge>
|
<Lozenge>{truncateMiddle(change.assetId)}</Lozenge>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -106,7 +108,7 @@ export const ProposalHeader = ({
|
|||||||
return (
|
return (
|
||||||
<div className="text-sm mb-2">
|
<div className="text-sm mb-2">
|
||||||
<div data-testid="proposal-title">
|
<div data-testid="proposal-title">
|
||||||
{useSubHeading ? (
|
{isListItem ? (
|
||||||
<header>
|
<header>
|
||||||
<SubHeading title={titleContent || t('Unknown proposal')} />
|
<SubHeading title={titleContent || t('Unknown proposal')} />
|
||||||
</header>
|
</header>
|
||||||
@ -121,9 +123,21 @@ export const ProposalHeader = ({
|
|||||||
<Lozenge variant={Intent.None}>{proposalType}</Lozenge>
|
<Lozenge variant={Intent.None}>{proposalType}</Lozenge>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
{description && (
|
<div className="flex items-center gap-2">
|
||||||
<div data-testid="proposal-description">{description}</div>
|
{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>
|
</div>
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ export const Proposal = ({ proposal }: ProposalProps) => {
|
|||||||
return (
|
return (
|
||||||
<AsyncRenderer data={params} loading={loading} error={error}>
|
<AsyncRenderer data={params} loading={loading} error={error}>
|
||||||
<section data-testid="proposal">
|
<section data-testid="proposal">
|
||||||
<ProposalHeader proposal={proposal} useSubHeading={false} />
|
<ProposalHeader proposal={proposal} isListItem={false} />
|
||||||
<div className="mb-10">
|
<div className="mb-10">
|
||||||
<ProposalChangeTable proposal={proposal} />
|
<ProposalChangeTable proposal={proposal} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -58,3 +58,39 @@
|
|||||||
.validators-table .ag-theme-balham-dark *:hover {
|
.validators-table .ag-theme-balham-dark *:hover {
|
||||||
cursor: pointer;
|
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;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user