Skip to content

Commit

Permalink
fix(APP-3495): Correctly forward web3 parameters to native ProposalAc…
Browse files Browse the repository at this point in the history
…tions components (#259)
  • Loading branch information
cgero-eth authored Aug 5, 2024
1 parent 7f72aed commit 43958f3
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 101 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

- Hide minimum participation details on `ProposalVotingBreakdownToken` module component when minParticipation is set
to zero.
- Correctly forward web3 params (e.g. `chainId`) to native `ProposalActions` components

## [1.0.41] - 2024-07-30

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const ProposalActions: React.FC<IProposalActionsProps> = (props) => {
action={action}
index={index}
name={actionNames?.[action.type]}
customComponent={customActionComponents?.[action.type]}
CustomComponent={customActionComponents?.[action.type]}
{...web3Props}
/>
))}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,25 @@ import { render, screen } from '@testing-library/react';
import { userEvent } from '@testing-library/user-event';
import { Accordion } from '../../../../../core';
import { modulesCopy } from '../../../../assets';
import {
generateProposalActionChangeMembers,
generateProposalActionChangeSettings,
generateProposalActionTokenMint,
generateProposalActionUpdateMetadata,
} from '../actions/generators';
import { generateProposalAction } from '../actions/generators/proposalAction';
import { generateProposalActionWithdrawToken } from '../actions/generators/proposalActionWithdrawToken';
import type { IProposalAction } from '../proposalActionsTypes';
import { type IProposalActionsActionProps, ProposalActionsAction } from './proposalActionsAction';

jest.mock('../actions', () => ({
ProposalActionWithdrawToken: () => <div data-testid="withdraw-token" />,
ProposalActionTokenMint: () => <div data-testid="token-mint" />,
ProposalActionUpdateMetadata: () => <div data-testid="update-metadata" />,
ProposalActionChangeMembers: () => <div data-testid="change-members" />,
ProposalActionChangeSettings: () => <div data-testid="change-settings" />,
}));

describe('<ProposalActionsAction /> component', () => {
const createTestComponent = (props?: Partial<IProposalActionsActionProps>) => {
const defaultProps: IProposalActionsActionProps = {
Expand All @@ -29,16 +43,16 @@ describe('<ProposalActionsAction /> component', () => {
});

it('renders custom action component when provided', async () => {
const customComponent = (props: { action: IProposalAction }) => `Custom action for ${props.action.type}`;
const CustomComponent = (props: { action: IProposalAction }) => props.action.type;
const action = generateProposalAction({
type: 'customType',
inputData: { function: 'transfer', contract: 'DAI', parameters: [] },
});

render(createTestComponent({ action, customComponent }));
render(createTestComponent({ action, CustomComponent }));

await userEvent.click(screen.getByText('transfer'));
expect(screen.getByText(`Custom action for ${action.type}`)).toBeInTheDocument();
expect(screen.getByText(action.type)).toBeInTheDocument();
});

it('renders action name when provided and contract is verified', () => {
Expand All @@ -47,4 +61,16 @@ describe('<ProposalActionsAction /> component', () => {
render(createTestComponent({ name, action }));
expect(screen.getByText(name)).toBeInTheDocument();
});

it.each([
{ action: generateProposalActionWithdrawToken(), testId: 'withdraw-token' },
{ action: generateProposalActionTokenMint(), testId: 'token-mint' },
{ action: generateProposalActionUpdateMetadata(), testId: 'update-metadata' },
{ action: generateProposalActionChangeMembers(), testId: 'change-members' },
{ action: generateProposalActionChangeSettings(), testId: 'change-settings' },
])('renders correct UI for $testId action', async ({ action, testId }) => {
render(createTestComponent({ action }));
await userEvent.click(screen.getByRole('button'));
expect(screen.getByTestId(testId)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { useMemo } from 'react';
import { Accordion, Heading } from '../../../../../core';
import type { IWeb3ComponentProps } from '../../../../types';
import { useOdsModulesContext } from '../../../odsModulesProvider';
import { ProposalActionsActionVerification } from '../proposalActionsActionVerfication/proposalActionsActionVerfication';
import {
ProposalActionChangeMembers,
ProposalActionChangeSettings,
ProposalActionTokenMint,
ProposalActionUpdateMetadata,
ProposalActionWithdrawToken,
} from '../actions';
import { ProposalActionsActionVerification } from '../proposalActionsActionVerfication';
import type { IProposalAction, ProposalActionComponent } from '../proposalActionsTypes';
import { proposalActionsUtils } from '../proposalActionsUtils';

Expand All @@ -21,33 +29,48 @@ export interface IProposalActionsActionProps extends IWeb3ComponentProps {
/**
* Custom component for the action
*/
customComponent?: ProposalActionComponent;
CustomComponent?: ProposalActionComponent;
}

export const ProposalActionsAction: React.FC<IProposalActionsActionProps> = (props) => {
const { action, index, name, customComponent, ...web3Props } = props;

const ActionComponent = customComponent ?? proposalActionsUtils.getActionComponent(action);
const { action, index, name, CustomComponent, ...web3Props } = props;

const { copy } = useOdsModulesContext();

const ActionComponent = useMemo(() => {
if (CustomComponent) {
return <CustomComponent action={action} {...web3Props} />;
}

if (proposalActionsUtils.isWithdrawTokenAction(action)) {
return <ProposalActionWithdrawToken action={action} {...web3Props} />;
} else if (proposalActionsUtils.isTokenMintAction(action)) {
return <ProposalActionTokenMint action={action} {...web3Props} />;
} else if (proposalActionsUtils.isUpdateMetadataAction(action)) {
return <ProposalActionUpdateMetadata action={action} {...web3Props} />;
} else if (proposalActionsUtils.isChangeMembersAction(action)) {
return <ProposalActionChangeMembers action={action} {...web3Props} />;
} else if (proposalActionsUtils.isChangeSettingsAction(action)) {
return <ProposalActionChangeSettings action={action} {...web3Props} />;
}

return null;
}, [action, CustomComponent, web3Props]);

const defaultTitle = name ?? action.inputData?.function;
const actionTitle = action.inputData == null ? copy.proposalActionsAction.notVerified : defaultTitle;

const isDisabled = action.inputData == null;

return (
<Accordion.Item value={isDisabled ? '' : `${index}`} disabled={isDisabled}>
<Accordion.ItemHeader>
<div className="flex flex-col items-start">
<Heading size="h4">
{action.inputData == null
? copy.proposalActionsAction.notVerified
: (name ?? action.inputData.function)}
</Heading>
<Heading size="h4">{actionTitle}</Heading>
<ProposalActionsActionVerification action={action} />
</div>
</Accordion.ItemHeader>
<Accordion.ItemContent>
{ActionComponent && <ActionComponent action={action} {...web3Props} />}
</Accordion.ItemContent>
<Accordion.ItemContent>{ActionComponent}</Accordion.ItemContent>
</Accordion.Item>
);
};
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import { render, screen } from '@testing-library/react';
import {
generateProposalAction,
generateProposalActionChangeMembers,
generateProposalActionChangeSettings,
generateProposalActionTokenMint,
Expand All @@ -10,75 +8,74 @@ import {
import { ProposalActionType } from './proposalActionsTypes';
import { proposalActionsUtils } from './proposalActionsUtils';

jest.mock('./actions', () => ({
ProposalActionWithdrawToken: () => <div>Mock ProposalActionWithdrawToken</div>,
ProposalActionUpdateMetadata: () => <div>Mock ProposalActionUpdateMetaData</div>,
ProposalActionTokenMint: () => <div>Mock ProposalActionTokenMint</div>,
ProposalActionChangeMembers: () => <div>Mock ProposalActionChangeMembers</div>,
ProposalActionChangeSettings: () => <div>Mock ProposalActionChangeSettings</div>,
}));

describe('ProposalActions utils', () => {
it('returns ProposalActionWithdrawToken component for withdrawToken action', () => {
const action = generateProposalActionWithdrawToken();

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionWithdrawToken')).toBeInTheDocument();
describe('isWithdrawTokenAction', () => {
it('returns true for withdraw action', () => {
const action = generateProposalActionWithdrawToken();
expect(proposalActionsUtils.isWithdrawTokenAction(action)).toBeTruthy();
});

it('returns false for other actions', () => {
const action = generateProposalActionUpdateMetadata();
expect(proposalActionsUtils.isWithdrawTokenAction(action)).toBeFalsy();
});
});

it('returns ProposalActionUpdateMetadata component for updateMetadata action', () => {
const action = generateProposalActionUpdateMetadata();

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionUpdateMetaData')).toBeInTheDocument();
describe('isChangeMemberAction', () => {
it('returns true for change members actions', () => {
const addMembersAction = generateProposalActionChangeMembers({ type: ProposalActionType.ADD_MEMBERS });
const removeMembersAction = generateProposalActionChangeMembers({
type: ProposalActionType.REMOVE_MEMBERS,
});
expect(proposalActionsUtils.isChangeMembersAction(addMembersAction)).toBeTruthy();
expect(proposalActionsUtils.isChangeMembersAction(removeMembersAction)).toBeTruthy();
});

it('returns false for other actions', () => {
const action = generateProposalActionWithdrawToken();
expect(proposalActionsUtils.isChangeMembersAction(action)).toBeFalsy();
});
});

it('returns ProposalActionTokenMint component for tokenMint action', () => {
const action = generateProposalActionTokenMint();
describe('isUpdateMetadataAction', () => {
it('returns true for update metadata action', () => {
const action = generateProposalActionUpdateMetadata();
expect(proposalActionsUtils.isUpdateMetadataAction(action)).toBeTruthy();
});

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionTokenMint')).toBeInTheDocument();
it('returns false for other actions', () => {
const action = generateProposalActionChangeMembers();
expect(proposalActionsUtils.isUpdateMetadataAction(action)).toBeFalsy();
});
});

it('returns ProposalActionChangeMembers component for addMember action', () => {
const action = generateProposalActionChangeMembers({ type: ProposalActionType.ADD_MEMBERS });
describe('isTokenMintAction', () => {
it('returns true for token mint action', () => {
const action = generateProposalActionTokenMint();
expect(proposalActionsUtils.isTokenMintAction(action)).toBeTruthy();
});

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionChangeMembers')).toBeInTheDocument();
it('returns false for other actions', () => {
const action = generateProposalActionUpdateMetadata();
expect(proposalActionsUtils.isTokenMintAction(action)).toBeFalsy();
});
});

it('returns ProposalActionChangeMembers component for removeMember action', () => {
const action = generateProposalActionChangeMembers({ type: ProposalActionType.REMOVE_MEMBERS });

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionChangeMembers')).toBeInTheDocument();
});

it('returns ProposalActionChangeSettings component for Multisig action', () => {
const action = generateProposalActionChangeSettings({ type: ProposalActionType.CHANGE_SETTINGS_MULTISIG });

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionChangeSettings')).toBeInTheDocument();
});

it('returns ProposalActionChangeSettings component for TokenVote action', () => {
const action = generateProposalActionChangeSettings({ type: ProposalActionType.CHANGE_SETTINGS_TOKENVOTE });

const Component = proposalActionsUtils.getActionComponent(action)!;
render(<Component />);
expect(screen.getByText('Mock ProposalActionChangeSettings')).toBeInTheDocument();
});

it('returns null for unknown action type', () => {
const action = generateProposalAction();

const Component = proposalActionsUtils.getActionComponent(action);
expect(Component).toBeNull();
describe('isChangeSettingsAction', () => {
it('returns true for change settings actions', () => {
const changeMultisig = generateProposalActionChangeSettings({
type: ProposalActionType.CHANGE_SETTINGS_MULTISIG,
});
const changeToken = generateProposalActionChangeSettings({
type: ProposalActionType.CHANGE_SETTINGS_TOKENVOTE,
});
expect(proposalActionsUtils.isChangeSettingsAction(changeMultisig)).toBeTruthy();
expect(proposalActionsUtils.isChangeSettingsAction(changeToken)).toBeTruthy();
});

it('returns false for other actions', () => {
const action = generateProposalActionTokenMint();
expect(proposalActionsUtils.isChangeSettingsAction(action)).toBeFalsy();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
import {
ProposalActionChangeMembers,
ProposalActionChangeSettings,
ProposalActionTokenMint,
ProposalActionUpdateMetadata,
ProposalActionWithdrawToken,
} from './actions';
import {
type IProposalAction,
type IProposalActionChangeMembers,
Expand All @@ -16,22 +9,6 @@ import {
} from './proposalActionsTypes';

class ProposalActionsUtils {
getActionComponent = (action: IProposalAction) => {
if (this.isWithdrawTokenAction(action)) {
return () => ProposalActionWithdrawToken({ action });
} else if (this.isTokenMintAction(action)) {
return () => ProposalActionTokenMint({ action });
} else if (this.isUpdateMetadataAction(action)) {
return () => ProposalActionUpdateMetadata({ action });
} else if (this.isChangeMembersAction(action)) {
return () => ProposalActionChangeMembers({ action });
} else if (this.isChangeSettingsAction(action)) {
return () => ProposalActionChangeSettings({ action });
}

return null;
};

isWithdrawTokenAction = (action: Partial<IProposalAction>): action is IProposalActionWithdrawToken => {
return action.type === ProposalActionType.WITHDRAW_TOKEN;
};
Expand Down

0 comments on commit 43958f3

Please sign in to comment.