Skip to content

Commit

Permalink
chore: Merge branch 'main' into f/app-2795
Browse files Browse the repository at this point in the history
  • Loading branch information
sepehr2github committed Mar 15, 2024
2 parents 3b469f8 + b9db5ee commit e7ee172
Show file tree
Hide file tree
Showing 33 changed files with 1,033 additions and 4 deletions.
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

### Added

- Implement `DaoDataListItem` module component
- Implement `AssetDataListItem` module component
- Implement `DaoDataListItem`, `ProposalDataListItem.Structure`, `AssetDataListItem` and `MemberDataListItem.Structure` module components
- Implement `StatePingAnimation` core component

### Changed

- Update `Tag` component primary variant styling

## [1.0.20] - 2024-03-13

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion src/core/components/tag/tag.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const variantToClassName: Record<TagVariant, string> = {
warning: 'bg-warning-200 text-warning-800',
critical: 'bg-critical-200 text-critical-800',
success: 'bg-success-200 text-success-800',
primary: 'bg-primary-100 text-primary-800',
primary: 'bg-primary-50 text-primary-400',
};

export const Tag: React.FC<ITagProps> = (props) => {
Expand Down
7 changes: 6 additions & 1 deletion src/modules/components/dao/daoAvatar/daoAvatar.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import classNames from 'classnames';
import type React from 'react';
import { Avatar, type IAvatarProps } from '../../../../core';
import { Avatar, type AvatarSize, type IAvatarProps } from '../../../../core';

export interface IDaoAvatarProps extends Omit<IAvatarProps, 'fallback'> {
/**
* Name of the DAO
*/
name?: string;
/**
* The size of the avatar.
* @default lg
*/
size?: AvatarSize;
}

export const DaoAvatar: React.FC<IDaoAvatarProps> = (props) => {
Expand Down
1 change: 1 addition & 0 deletions src/modules/components/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './dao';
export * from './member';
export * from './odsModulesProvider';
export * from './proposal';
1 change: 1 addition & 0 deletions src/modules/components/member/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './memberAvatar';
export * from './memberDataListItem';
1 change: 1 addition & 0 deletions src/modules/components/member/memberDataListItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './memberDataListItemStructure';
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { MemberDataListItemStructure } from './memberDataListItemStructure';

export const MemberDataListItem = {
Structure: MemberDataListItemStructure,
};

export type { IMemberDataListItemProps } from './memberDataListItemStructure';
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import type { Meta, StoryObj } from '@storybook/react';
import { DataList } from '../../../../../core';
import { MemberDataListItemStructure } from './memberDataListItemStructure';

const meta: Meta<typeof MemberDataListItemStructure> = {
title: 'Modules/Components/Member/MemberDataListItem.Structure',
component: MemberDataListItemStructure,
tags: ['autodocs'],
parameters: {
design: {
type: 'figma',
url: 'https://www.figma.com/file/ISSDryshtEpB7SUSdNqAcw/branch/P0GeJKqILL7UXvaqu5Jj7V/Aragon-ODS?type=design&node-id=14385%3A30819&mode=dev',
},
},
};

type Story = StoryObj<typeof MemberDataListItemStructure>;

/**
* Default usage example of the MemberDataList module component.
*/
export const Default: Story = {
args: {
address: '0x1234567890123456789012345678901234567890',
},
render: (args) => (
<DataList.Root entityLabel="Members">
<DataList.Container>
<MemberDataListItemStructure {...args} />
</DataList.Container>{' '}
</DataList.Root>
),
};

/**
* Example of the MemberDataList module component with fully loaded props and token voting.
*/
export const Loaded: Story = {
args: {
isDelegate: true,
ensName: 'vitalik.eth',
address: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
delegationCount: 9,
votingPower: 13370,
},
render: (args) => (
<DataList.Root entityLabel="Members">
<DataList.Container>
<MemberDataListItemStructure {...args} />
</DataList.Container>{' '}
</DataList.Root>
),
};

export default meta;
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import { getAddress, isAddress } from 'viem';
import { useAccount } from 'wagmi';
import { DataList } from '../../../../../core';
import { MemberDataListItemStructure, type IMemberDataListItemProps } from './memberDataListItemStructure';

jest.mock('viem', () => ({
isAddress: jest.fn(),
getAddress: jest.fn(),
}));

jest.mock('viem/ens', () => ({
normalize: jest.fn(),
}));

jest.mock('wagmi', () => ({
useAccount: jest.fn(),
}));

jest.mock('../../memberAvatar', () => ({ MemberAvatar: () => <div data-testid="member-avatar-mock" /> }));

describe('<MemberDataListItem /> component', () => {
const createTestComponent = (props?: Partial<IMemberDataListItemProps>) => {
const completeProps: IMemberDataListItemProps = {
address: '0x1234567890123456789012345678901234567890',
...props,
};

return (
<DataList.Root entityLabel="Members">
<DataList.Container>
<MemberDataListItemStructure {...completeProps} />
</DataList.Container>
</DataList.Root>
);
};

beforeEach(() => {
(isAddress as unknown as jest.Mock).mockImplementation(() => true);
(getAddress as jest.Mock).mockImplementation((address: string) => address);

(useAccount as jest.Mock).mockReturnValue({
address: '0x1234567890123456789012345678901234567890',
isConnected: true,
});
});

it('renders the avatar', async () => {
render(createTestComponent());
const avatar = screen.getByTestId('member-avatar-mock');

expect(avatar).toBeInTheDocument();
});

it('conditionally renders the "Your Delegate" tag', async () => {
const address = '0x0987654321098765432109876543210987654321';
render(createTestComponent({ isDelegate: true, address }));

expect(screen.getByText('Your Delegate')).toBeInTheDocument();
});

it('renders the ENS user handle instead of address if provided', async () => {
const ensName = 'testUserHandle';
render(createTestComponent({ ensName }));

expect(screen.getByRole('heading', { name: ensName })).toBeInTheDocument();
expect(
screen.queryByRole('heading', { name: '0x1234567890123456789012345678901234567890' }),
).not.toBeInTheDocument();
});

it('conditionally renders the delegation count and formats it', async () => {
const { rerender } = render(createTestComponent({ delegationCount: 340 }));
const delegationText = await screen.findByText(/340/);
// eslint-disable-next-line testing-library/no-node-access
const parentElement = delegationText.closest('h2');

expect(parentElement).toHaveTextContent('340 Delegation');

rerender(createTestComponent({ delegationCount: 2959 }));
const delegationsText = await screen.findByText(/2.96K/);
// eslint-disable-next-line testing-library/no-node-access
const parentElementForTwo = delegationsText.closest('h2');

expect(parentElementForTwo).toHaveTextContent('2.96K Delegations');
});

it('renders the voting power, correctly formatting large numbers', async () => {
const votingPower = 420689;
render(createTestComponent({ votingPower }));
const formattedNumberElement = await screen.findByText(/420\.69K/);
// eslint-disable-next-line testing-library/no-node-access
const parentElement = formattedNumberElement.closest('h2');

expect(parentElement).toHaveTextContent('420.69K Voting Power');
});

it('renders the "You" tag when the user is the current account', async () => {
const address = '0x1234567890123456789012345678901234567890';
render(createTestComponent({ address }));

expect(screen.getByText('You')).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { getAddress } from 'viem';
import { useAccount } from 'wagmi';
import { DataList, Heading, NumberFormat, Tag, formatterUtils, type IDataListItemProps } from '../../../../../core';
import { truncateEthAddress } from '../../../../utils/truncateEthereumAddress';
import { MemberAvatar } from '../../memberAvatar';

export interface IMemberDataListItemProps extends IDataListItemProps {
/**
* Whether the member is a delegate of current user or not.
*/
isDelegate?: boolean;
/**
* The number of delegations the member has from other members.
*/
delegationCount?: number;
/**
* The total voting power of the member.
*/
votingPower?: number;
/**
* ENS name of the user.
*/
ensName?: string;
/**
* 0x address of the user.
*/
address: string;
/**
* Direct URL src of the user avatar image to be rendered.
*/
avatarSrc?: string;
}

export const MemberDataListItemStructure: React.FC<IMemberDataListItemProps> = (props) => {
const { isDelegate, delegationCount = 0, votingPower = 0, avatarSrc, ensName, address, ...otherProps } = props;

const { address: currentUserAddress, isConnected } = useAccount();

const isCurrentUser = isConnected && address && currentUserAddress === getAddress(address);

const resolvedUserHandle = ensName != null && ensName !== '' ? ensName : address;

const hasDelegationOrVotingPower = delegationCount > 0 || votingPower > 0;

return (
<DataList.Item className="min-w-fit !py-0 px-4 md:px-6" {...otherProps}>
<div className="flex flex-col items-start justify-center gap-y-3 py-4 md:min-w-44 md:py-6">
<div className="flex w-full items-center justify-between">
<MemberAvatar
ensName={ensName}
address={address}
avatarSrc={avatarSrc}
responsiveSize={{ md: 'md' }}
/>
{isDelegate && !isCurrentUser && <Tag variant="info" label="Your Delegate" />}
{isCurrentUser && <Tag variant="neutral" label="You" />}
</div>

<Heading className="inline-block w-full truncate" size="h2" as="h1">
{truncateEthAddress(resolvedUserHandle)}
</Heading>

{hasDelegationOrVotingPower && (
<div className="flex flex-col gap-y-2">
{delegationCount > 0 && (
<Heading size="h5" as="h2">
{formatterUtils.formatNumber(delegationCount, { format: NumberFormat.GENERIC_SHORT })}
<span className="text-neutral-500">{` Delegation${delegationCount === 1 ? '' : 's'}`}</span>
</Heading>
)}
{votingPower > 0 && (
<Heading size="h5" as="h2">
{formatterUtils.formatNumber(votingPower, { format: NumberFormat.GENERIC_SHORT })}
<span className="text-neutral-500"> Voting Power</span>
</Heading>
)}
</div>
)}
</div>
</DataList.Item>
);
};
1 change: 1 addition & 0 deletions src/modules/components/proposal/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './proposalDataListItem';
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { render, screen } from '@testing-library/react';
import { NumberFormat, formatterUtils } from '../../../../../core';
import { ApprovalThresholdResult, type IApprovalThresholdResultProps } from './approvalThresholdResult';

describe('<ApprovalThresholdResult /> component', () => {
const createTestComponent = (props?: Partial<IApprovalThresholdResultProps>) => {
const completeProps: IApprovalThresholdResultProps = {
approvalAmount: 1,
approvalThreshold: 2,
...props,
};

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

it('renders the formatted approval threshold and approval amount and the corresponding progressbar', () => {
const mockProps: IApprovalThresholdResultProps = {
approvalAmount: 15000000,
approvalThreshold: 20000000,
};

render(createTestComponent(mockProps));

const expectedApproval = formatterUtils.formatNumber(mockProps.approvalAmount, {
format: NumberFormat.GENERIC_SHORT,
}) as string;

const expectedThreshold = formatterUtils.formatNumber(mockProps.approvalThreshold, {
format: NumberFormat.GENERIC_SHORT,
}) as string;

const progressbar = screen.getByRole('progressbar');
expect(progressbar).toBeInTheDocument();

const expectedPercentage = (mockProps.approvalAmount / mockProps.approvalThreshold) * 100;
expect(progressbar.getAttribute('data-value')).toEqual(expectedPercentage.toString());

expect(screen.getByText(expectedApproval)).toBeInTheDocument();
expect(screen.getByText(expectedThreshold)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { NumberFormat, Progress, formatterUtils } from '../../../../../core';
import { type IApprovalThresholdResult } from '../proposalDataListItemStructure';

export interface IApprovalThresholdResultProps extends IApprovalThresholdResult {}

/**
* `ApprovalThresholdResult` component
*/
export const ApprovalThresholdResult: React.FC<IApprovalThresholdResultProps> = (props) => {
const { approvalAmount, approvalThreshold } = props;
const percentage = approvalThreshold !== 0 ? (approvalAmount / approvalThreshold) * 100 : 100;

return (
// TODO: apply internationalization to Approved By, of, and Members [APP-2627]
<div className="flex w-full flex-col gap-y-2 rounded-xl border border-neutral-100 bg-neutral-0 px-4 py-3 shadow-neutral-sm md:gap-y-3 md:px-6 md:py-5">
<div className="flex flex-1 gap-x-4 leading-tight text-neutral-800 md:gap-x-6 md:text-lg">
<span className="flex-1">Approved By</span>
</div>
<Progress value={percentage} />
<div className="flex gap-x-0.5 leading-tight text-neutral-500 md:gap-x-1 md:text-lg">
<span className="text-primary-400">
{formatterUtils.formatNumber(approvalAmount, { format: NumberFormat.GENERIC_SHORT })}
</span>
<span>of</span>
<span>{formatterUtils.formatNumber(approvalThreshold, { format: NumberFormat.GENERIC_SHORT })}</span>
<span>Members</span>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ApprovalThresholdResult, type IApprovalThresholdResultProps } from './approvalThresholdResult';
13 changes: 13 additions & 0 deletions src/modules/components/proposal/proposalDataListItem/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ProposalDataListItemStructure as Structure } from './proposalDataListItemStructure';

export const ProposalDataListItem = {
Structure,
};

export type {
IApprovalThresholdResult,
IMajorityVotingResult,
IProposalDataListItemStructureProps,
ProposalStatus,
ProposalType,
} from './proposalDataListItemStructure';
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { MajorityVotingResult, type IMajorityVotingResultProps } from './majorityVotingResult';
Loading

0 comments on commit e7ee172

Please sign in to comment.