Skip to content

Commit

Permalink
feat: APP-3094 - Update ProposalDataListItemStrucure component (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fabricevladimir authored Apr 23, 2024
1 parent 891937d commit 1a9cb09
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 106 deletions.
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Added

- Implement `ProposalDataListItemSkeleton` module component
- Extend `addressUtils` with `isAddressEqual` method

### Changed

- Remove padding from `DataListContainer`, `DataListFilterStatus`, `DataListPagination` and `DataListRoot`
- Add `stageId` and `stageTitle` properties to `IApprovalThresholdResult` & `IMajorityVotingResult` interfaces
- Add `id` and optional `tag` properties to `ProposalDataListItemStructure`
- Remove `publisherProfileLink` and `protocolUpdate` properties from `ProposalDataListItemStructure`
- Update `date` and `result` properties of `ProposalDataListItemStructure` to be optional and `publisher` to allow for
multiple publishers

### Fixed

- `Link` core component to truncate on overflow

## [1.0.23] - 2024-04-18

Expand Down
4 changes: 2 additions & 2 deletions src/core/components/link/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export const Link = React.forwardRef<HTMLAnchorElement, ILinkProps>((props, ref)
{...(disabled && { tabIndex: -1, 'aria-disabled': 'true' })}
{...otherProps}
>
<div className="flex items-center gap-x-2 truncate">
{children}
<div className="flex items-center gap-x-2">
<span className="truncate">{children}</span>
{iconRight && <Icon icon={iconRight} size="sm" />}
</div>
{description && <p className={descriptionClassName}>{description}</p>}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,22 @@ describe('<ApprovalThresholdResult /> component', () => {
expect(screen.getByText(expectedApproval)).toBeInTheDocument();
expect(screen.getByText(expectedThreshold)).toBeInTheDocument();
});

it('renders the stage title and stage id when provided', () => {
const stage = {
title: 'Test Stage',
id: '3',
};

render(createTestComponent({ stage }));

expect(screen.getByText(stage.title)).toBeInTheDocument();
expect(screen.getByText(stage.id)).toBeInTheDocument();
});

it('renders the default stage title when not provided', () => {
render(createTestComponent());

expect(screen.getByText(/approved by/i)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ export interface IApprovalThresholdResultProps extends IApprovalThresholdResult
* `ApprovalThresholdResult` component
*/
export const ApprovalThresholdResult: React.FC<IApprovalThresholdResultProps> = (props) => {
const { approvalAmount, approvalThreshold } = props;
const { approvalAmount, approvalThreshold, stage } = props;
const percentage = approvalThreshold !== 0 ? (approvalAmount / approvalThreshold) * 100 : 100;

return (
// TODO: apply internationalization to Approved By, of, and Members [APP-2627]
// TODO: apply internationalization to Approved By, of, Stage, 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 className="flex flex-1 gap-x-3 leading-tight text-neutral-800 md:gap-x-6 md:text-lg">
<span className="line-clamp-1 flex-1">{stage?.title ?? 'Approved By'}</span>
{stage?.id != null && (
<span className="flex shrink-0 justify-between gap-x-0.5">
<span className="flex-1 text-neutral-500">Stage</span>
{stage.id}
</span>
)}
</div>
<Progress value={percentage} />
<div className="flex gap-x-0.5 leading-tight text-neutral-500 md:gap-x-1 md:text-lg">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,22 @@ describe('<MajorityVotingResult /> component', () => {
expect(screen.getByText(mockProps.votePercentage, { exact: false })).toBeInTheDocument();
expect(screen.getByRole('progressbar')).toBeInTheDocument();
});

it('renders the stage title and stage id when provided', () => {
const stage = {
title: 'Test Stage',
id: '3',
};

render(createTestComponent({ stage }));

expect(screen.getByText(stage.title)).toBeInTheDocument();
expect(screen.getByText(stage.id)).toBeInTheDocument();
});

it('renders the default stage title when not provided', () => {
render(createTestComponent());

expect(screen.getByText(/winning option/i)).toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,20 @@ export interface IMajorityVotingResultProps extends IMajorityVotingResult {}
* `MajorityVotingResult` component
*/
export const MajorityVotingResult: React.FC<IMajorityVotingResultProps> = (props) => {
const { option, voteAmount, votePercentage } = props;
const { option, stage, voteAmount, votePercentage } = props;

return (
// TODO: apply internationalization to Winning Option [APP-2627]
// TODO: apply internationalization to Winning Option and Stage [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">Winning Option</span>
<span className="text-primary-400">{`${votePercentage}%`}</span>
<span className="line-clamp-1 flex-1">{stage?.title ?? 'Winning Option'}</span>
{stage?.id == null && <span className="text-primary-400">{`${votePercentage}%`}</span>}
{stage?.id != null && (
<span className="flex shrink-0 justify-between gap-x-0.5">
<span className="flex-1 text-neutral-500">Stage</span>
{stage.id}
</span>
)}
</div>
<Progress value={votePercentage} />
<div className="flex gap-x-4 leading-tight md:gap-x-6 md:text-lg">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ describe('<ProposalDataListItemStatus /> component', () => {
expect(screen.getByTestId(IconType.CALENDAR)).toBeInTheDocument();
});

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

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

expect(screen.queryByTestId(IconType.CALENDAR)).not.toBeInTheDocument();
});

it("only displays the date for proposals with a status that is not 'draft'", () => {
const date = 'test date';
const status = 'draft';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,9 @@ export const ProposalDataListItemStatus: React.FC<IProposalDataListItemStatusPro
</span>
{ongoingAndVoted && <AvatarIcon icon={IconType.CHECKMARK} responsiveSize={{ md: 'md' }} />}
{ongoing && !voted && <StatePingAnimation variant={ongoingStatusToPingVariant[status]} />}
{!ongoing && !voted && <AvatarIcon icon={IconType.CALENDAR} responsiveSize={{ md: 'md' }} />}
{!ongoing && !voted && date && (
<AvatarIcon icon={IconType.CALENDAR} responsiveSize={{ md: 'md' }} />
)}
</div>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,25 @@ export interface IProposalDataListItemStructureBaseProps<TType extends ProposalT
extends IDataListItemProps,
IWeb3ComponentProps {
/**
* Indicates date relative to the proposal status
* Proposal id
*/
date: string;
id?: string;
/**
* Indicates whether the proposal is a protocol update
* Indicates date relative to the proposal status
*/
protocolUpdate?: boolean;
date?: string;
/**
* Publisher address (and optional ENS name)
* Optional tag indicating proposal type
*/
publisher: ICompositeAddress;
tag?: string;
/**
* Link to the publisher's profile
* Publisher(s) address (and optional ENS name and profile link)
*/
publisherProfileLink: string;
publisher: IPublisher | IPublisher[];
/**
* Result of the proposal shown only when it is active, challenged or vetoed.
*/
result: TType extends 'majorityVoting' ? IMajorityVotingResult : IApprovalThresholdResult;
result?: TType extends 'majorityVoting' ? IMajorityVotingResult : IApprovalThresholdResult;
/**
* Proposal status
*/
Expand All @@ -60,8 +60,33 @@ export interface IProposalDataListItemStructureBaseProps<TType extends ProposalT
*/
voted?: boolean;
}
export interface IPublisher extends ICompositeAddress {
/**
* Link to additional information about the publisher, such as a profile page or block explorer.
*/
link?: string;
}

export interface IProposalStage {
/**
* Name of the proposal stage
*/
title?: string;

/**
* Id of the proposal stage
*/
id: string | number;
}

export interface IProposalResultBase {
/**
* Proposal stage
*/
stage?: IProposalStage;
}

export interface IApprovalThresholdResult {
export interface IApprovalThresholdResult extends IProposalResultBase {
/**
* Number of approvals for the proposal
*/
Expand All @@ -72,7 +97,7 @@ export interface IApprovalThresholdResult {
approvalThreshold: number;
}

export interface IMajorityVotingResult {
export interface IMajorityVotingResult extends IProposalResultBase {
/**
* Winning option
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ const meta: Meta<typeof ProposalDataListItem.Structure> = {

type Story = StoryObj<typeof ProposalDataListItem.Structure>;

const baseArgs: Omit<IProposalDataListItemStructureProps, 'result'> = {
const basePublisher = {
address: '0xd5fb864ACfD6BB2f72939f122e89fF7F475924f5',
link: 'https://app.aragon.org/#/daos/base/0xd2705c56aa4edb98271cb8cea2b0df3288ad4585/members/0xd5fb864ACfD6BB2f72939f122e89fF7F475924f5',
};

const baseArgs: Omit<IProposalDataListItemStructureProps, 'result' | 'publisher'> = {
date: '5 days left',
protocolUpdate: false,
publisher: { address: '0xd5fb864ACfD6BB2f72939f122e89fF7F475924f5' },
publisherProfileLink:
'https://app.aragon.org/#/daos/base/0xd2705c56aa4edb98271cb8cea2b0df3288ad4585/members/0xd5fb864ACfD6BB2f72939f122e89fF7F475924f5',
status: 'draft',
title: 'This is a very serious proposal to send funds to a wallet address',
summary: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris vel eleifend neque, in mattis eros.
Expand All @@ -39,6 +40,7 @@ const baseArgs: Omit<IProposalDataListItemStructureProps, 'result'> = {
export const MajorityVoting: Story = {
args: {
...baseArgs,
publisher: { ...basePublisher },
type: 'majorityVoting',
result: {
option: 'yes',
Expand All @@ -61,7 +63,7 @@ export const MajorityVoting: Story = {
export const ApprovalThreshold: Story = {
args: {
...baseArgs,
publisher: { name: 'sio.eth', address: baseArgs.publisher.address },
publisher: { ...basePublisher, name: 'sio.eth' },
type: 'approvalThreshold',
result: {
approvalAmount: 4,
Expand All @@ -77,4 +79,33 @@ export const ApprovalThreshold: Story = {
),
};

/**
* Example of the `ProposalDataListItem.Structure` module component for a multi-body proposal.
*/
export const MultiBody: Story = {
args: {
...baseArgs,
id: 'PIP-1',
publisher: [
{ ...basePublisher, name: '0xRugg', link: undefined },
{ ...basePublisher, name: 'Bob the Builder', link: undefined },
{ ...basePublisher, name: 'sio.eth' },
{ ...basePublisher },
],
type: 'approvalThreshold',
result: {
stage: { title: 'Founders Approval Council', id: '1' },
approvalAmount: 4,
approvalThreshold: 6,
},
},
render: (props) => (
<DataList.Root entityLabel="Proposals">
<DataList.Container SkeletonElement={ProposalDataListItem.Skeleton}>
<ProposalDataListItem.Structure {...props} />
</DataList.Container>
</DataList.Root>
),
};

export default meta;
Loading

0 comments on commit 1a9cb09

Please sign in to comment.