Skip to content

Commit

Permalink
feat(APP-3393): Update handling of proposal statuses (#267)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgero-eth authored Aug 7, 2024
1 parent 79005f2 commit dbd3e9c
Show file tree
Hide file tree
Showing 25 changed files with 271 additions and 249 deletions.
10 changes: 7 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,25 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Update `<Wallet />` module component to only resolve user ENS name when name property is not set
- Fix expand behaviour of `TextAreaRichText` core component when used inside a dialog and hide the input label
- Fix `NumberInput` component to correctly update values on plus / minus buttons click
- Fix `ProposalVotingBreakdownToken` module component to display correct progress variant when min-participation and
support are equal to the threshold required

### Added

- Update `<Wallet />` module component to support custom `chainId` and `wagmi` configurations
- Update ICompositeAddress interface and components using it to support custom avatar
- Add z-index property customisation for `TextAreaRichText` core component when expanded
- Handle `useFocusTrap` property on dialog components to support disabling default focus-trap behaviour
- Add 'Raw' and 'Decoded' views to `ProposalActions` with Dropdown selector for the data of proposal actions with more
technical understanding.
- Add `AlertCard` to `ProposalActionsAction` to alert user when action will send native currency
- Update `ICompositeAddress` interface and components using it to support custom avatar
- Make `ProposalStatus` strings customisable
- Implement and export `proposalStatusToVotingStatus` utility
- Add 'Raw' and 'Decoded' views to `ProposalActions` module component

### Changed

- Update minor and patch NPM dependencies
- Bump `postcss` from 8.4.40 to 8.4.41
- Update `ProposalStatus` type to enum to align it with `ProposalVotingStatus` enum

## [1.0.41] - 2024-07-30

Expand Down
14 changes: 14 additions & 0 deletions src/modules/assets/copy/modulesCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ export const modulesCopy = {
voted: "You've voted",
ago: 'ago',
left: 'left',
statusLabel: {
ACCEPTED: 'Accepted',
ACTIVE: 'Active',
CHALLENGED: 'Challenged',
DRAFT: 'Draft',
EXECUTED: 'Executed',
EXPIRED: 'Expired',
FAILED: 'Failed',
PARTIALLY_EXECUTED: 'Partially executed',
PENDING: 'Pending',
EXECUTABLE: 'Executable',
REJECTED: 'Rejected',
VETOED: 'Vetoed',
},
},
proposalDataListItemStructure: {
by: 'By',
Expand Down
1 change: 1 addition & 0 deletions src/modules/components/proposal/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './proposalActions';
export * from './proposalDataListItem';
export * from './proposalUtils';
export * from './proposalVoting';
Original file line number Diff line number Diff line change
Expand Up @@ -40,14 +40,11 @@ export const MixedActions: Story = {
priceUsd: '2800',
}),
}),
generateProposalActionUpdateMetadata({
data: '0x3f60b63300000000000000000000000019dbc1c820dd3f13260829a4e06dda6d9ef758db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d5fb864acfd6bb2f72939f122e89ff7f475924f5',
}),
generateProposalActionUpdateMetadata({ data: 'update-data' }),
generateProposalAction({
to: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
value: '10',
data: '0x3f60b63300000000000000000000000019dbc1c820dd3f13260829a4e06dda6d9ef758db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d5fb864acfd6bb2f72939f122e89ff7f475924f5',

value: '1000000000000000000',
data: 'custom-action-data',
inputData: {
function: 'customAction',
contract: 'GovernanceERC20',
Expand All @@ -69,7 +66,8 @@ export const MixedActions: Story = {
}),
generateProposalAction({
type: 'unknownType',
data: '0x3f60b63300000000000000000000000019dbc1c820dd3f13260829a4e06dda6d9ef758db00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000d5fb864acfd6bb2f72939f122e89ff7f475924f5',
data: 'data-mock',
to: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
inputData: null,
}),
],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { Accordion } from '../../../../../core';
import { Accordion, IconType } from '../../../../../core';
import { modulesCopy } from '../../../../assets';
import {
generateProposalActionChangeMembers,
Expand Down Expand Up @@ -111,42 +111,20 @@ describe('<ProposalActionsAction /> component', () => {
expect(screen.getByTestId(testId)).toBeInTheDocument();
});

it('renders an alert when action.value is not "0" and data is native "0x"', async () => {
const action = generateProposalAction({
inputData: { function: '', contract: '', parameters: [] },
value: '1000000000000000000',
data: '0x',
});
it('renders an alert when action has value is not "0" and is not a native transfer', async () => {
const action = generateProposalAction({ value: '1000000000000000000', data: 'some-data' });
render(createTestComponent({ action }));

await userEvent.click(screen.getByRole('button'));

expect(screen.getByRole('alert')).toBeInTheDocument();
expect(screen.getByTestId(IconType.CRITICAL)).toBeInTheDocument();
});

it('does not render alert when action.value is "0" and action.data is not "0x"', async () => {
const action = generateProposalAction({
inputData: { function: '', contract: '', parameters: [] },
value: '0',
data: '0x1234',
});
it('does not render an alert when action has value but it is a native transfer', async () => {
const action = generateProposalAction({ value: '0', data: '0x' });
render(createTestComponent({ action }));

await userEvent.click(screen.getByRole('button'));

expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});

it('does not render alert when action.value is "0" and action.data is "0x"', async () => {
const action = generateProposalAction({
inputData: { function: '', contract: '', parameters: [] },
value: '0',
data: '0x',
});
render(createTestComponent({ action }));

await userEvent.click(screen.getByRole('button'));

expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useMemo, useRef, useState } from 'react';
import { Accordion, AlertCard, Heading } from '../../../../../core';
import { Accordion, AlertCard, Heading, Icon, IconType } from '../../../../../core';
import type { IWeb3ComponentProps } from '../../../../types';
import { useOdsModulesContext } from '../../../odsModulesProvider';
import {
Expand Down Expand Up @@ -89,23 +89,30 @@ export const ProposalActionsAction: React.FC<IProposalActionsActionProps> = (pro
itemRef.current.scrollIntoView({ behavior: 'instant', block: 'center' });
}
};
const isNativeTransfer = action.value !== '0' && action.data === '0x';

// Display value warning when a transaction is sending value but it's not a native transfer (data !== '0x')
const displayValueWarning = action.value !== '0' && action.data !== '0x';

return (
<Accordion.Item value={`${index}`} ref={itemRef}>
<Accordion.ItemHeader>
<div className="flex flex-col items-start">
<Heading size="h4">
{action.inputData == null
? copy.proposalActionsAction.notVerified
: (name ?? action.inputData.function)}
</Heading>
<div className="flex flex-row items-center gap-2">
<Heading size="h4" className={displayValueWarning ? '!text-critical-800' : undefined}>
{action.inputData == null
? copy.proposalActionsAction.notVerified
: (name ?? action.inputData.function)}
</Heading>
{displayValueWarning && (
<Icon icon={IconType.CRITICAL} size="md" className="text-critical-500" />
)}
</div>
<ProposalActionsActionVerification action={action} />
</div>
</Accordion.ItemHeader>
<Accordion.ItemContent ref={contentRef}>
<div className="flex flex-col items-start gap-y-6 self-start md:gap-y-8">
{isNativeTransfer && (
{displayValueWarning && (
<AlertCard
variant="critical"
message={copy.proposalActionsAction.nativeSendAlert}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { InputNumber, InputText } from '../../../../../../core';
import { InputText } from '../../../../../../core';
import { useOdsModulesContext } from '../../../../odsModulesProvider';
import type { IProposalAction } from '../../proposalActionsTypes';

Expand All @@ -15,12 +15,11 @@ export const ProposalActionsActionDecodedView: React.FC<IProposalActionsActionDe

return (
<div className="flex w-full flex-col gap-y-3">
<InputNumber
<InputText
label={copy.proposalActionsActionDecodedView.valueLabel}
helpText={copy.proposalActionsActionDecodedView.valueHelper}
value={action.value ?? 0}
value={action.value}
disabled={true}
suffix="ETH"
/>
{action.inputData?.parameters.map((parameter) => (
<InputText
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, clipboardUtils, InputNumber, InputText, TextArea } from '../../../../../../core';
import { Button, clipboardUtils, InputText, TextArea } from '../../../../../../core';
import { useOdsModulesContext } from '../../../../odsModulesProvider';
import type { IProposalAction } from '../../proposalActionsTypes';

Expand All @@ -17,16 +17,8 @@ export const ProposalActionsActionRawView: React.FC<IProposalActionsActionRawVie
return (
<div className="flex w-full flex-col gap-y-3">
<InputText label={copy.proposalActionsActionRawView.to} value={action.to} disabled={true} />

<InputNumber
label={copy.proposalActionsActionRawView.value}
value={action.value}
disabled={true}
suffix="ETH"
/>

<InputText label={copy.proposalActionsActionRawView.value} value={action.value} disabled={true} />
<TextArea label={copy.proposalActionsActionRawView.data} value={action.data} disabled={true} />

<Button className="self-end" variant="tertiary" size="md" onClick={() => clipboardUtils.copy(action.data)}>
{copy.proposalActionsActionRawView.copyButton}
</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,34 @@
import { render, screen } from '@testing-library/react';
import { DateFormat, IconType, formatterUtils } from '../../../../../core';
import { type ProposalStatus } from '../proposalDataListItemStructure';
import { modulesCopy } from '../../../../assets';
import { ProposalStatus } from '../../proposalUtils';
import { proposalDataListItemUtils } from '../proposalDataListItemUtils';
import { ProposalDataListItemStatus, type IProposalDataListItemStatusProps } from './proposalDataListItemStatus';

describe('<ProposalDataListItemStatus /> component', () => {
const createTestComponent = (props?: Partial<IProposalDataListItemStatusProps>) => {
const completeProps: IProposalDataListItemStatusProps = {
status: 'accepted',
status: ProposalStatus.ACCEPTED,
...props,
};

return <ProposalDataListItemStatus {...completeProps} />;
};

const ongoingStatuses = ['active', 'challenged', 'vetoed'];

it('displays the date, calendar icon and status', () => {
const date = 1719563030308;
const status = 'accepted';
const status = ProposalStatus.ACCEPTED;

render(createTestComponent({ date, status }));

const formattedDate = formatterUtils.formatDate(date, { format: DateFormat.RELATIVE })!;
expect(screen.getByText(formattedDate)).toBeInTheDocument();
expect(screen.getByText(status)).toBeInTheDocument();
expect(screen.getByText(modulesCopy.proposalDataListItemStatus.statusLabel[status])).toBeInTheDocument();
expect(screen.getByTestId(IconType.CALENDAR)).toBeInTheDocument();
});

it('does not render the calendar icon and date when date property is not defined', () => {
const status = 'accepted';
const status = ProposalStatus.ACCEPTED;
const date = undefined;

render(createTestComponent({ status, date }));
Expand All @@ -40,38 +40,40 @@ describe('<ProposalDataListItemStatus /> component', () => {

it("only displays the date for proposals with a status that is not 'draft'", () => {
const date = 1719563030308;
const status = 'draft';
const status = ProposalStatus.DRAFT;

render(createTestComponent({ date, status }));

const formattedDate = formatterUtils.formatDate(date, { format: DateFormat.RELATIVE })!;
expect(screen.getByText(status)).toBeInTheDocument();
expect(screen.getByText(modulesCopy.proposalDataListItemStatus.statusLabel[status])).toBeInTheDocument();
expect(screen.queryByText(formattedDate)).not.toBeInTheDocument();
expect(screen.queryByTestId(IconType.CALENDAR)).not.toBeInTheDocument();
});

ongoingStatuses.forEach((status) => {
it(`displays the date and a pinging indicator when the status is '${status}' and voted is false`, () => {
test.each(proposalDataListItemUtils.ongoingStatuses)(
'displays the date and a pinging indicator when the status is %s and voted is false',
(status) => {
const date = 1719563030308;
render(createTestComponent({ date, status: status as ProposalStatus, voted: false }));

const formattedDate = formatterUtils.formatDate(date, { format: DateFormat.RELATIVE })!;
expect(screen.getByText(formattedDate)).toBeInTheDocument();
expect(screen.getByTestId('statePingAnimation')).toBeInTheDocument();
});
});
},
);

ongoingStatuses.forEach((status) => {
it(`displays 'You've voted' with an icon checkmark when the status is '${status}' and voted is true`, () => {
test.each(proposalDataListItemUtils.ongoingStatuses)(
'displays a you-voted label with an icon checkmark when the status is %s and voted is true',
(status) => {
render(createTestComponent({ status: status as ProposalStatus, voted: true }));

expect(screen.getByText(/You've voted/i)).toBeInTheDocument();
expect(screen.getByTestId(IconType.CHECKMARK)).toBeInTheDocument();
});
});
},
);

it("does not display 'You've voted' when the status is not an ongoing one and the voted is true", () => {
render(createTestComponent({ status: 'executed', voted: true }));
it('does not display a you-voted label when the status is not an ongoing one and the voted is true', () => {
render(createTestComponent({ status: ProposalStatus.EXECUTED, voted: true }));

expect(screen.queryByText(/You've voted/i)).not.toBeInTheDocument();
expect(screen.queryByTestId(IconType.CHECKMARK)).not.toBeInTheDocument();
Expand Down
Loading

0 comments on commit dbd3e9c

Please sign in to comment.