-
Proposals
+
+
Proposals
setSearchTerm(e.target.value)}
/>
-
+
-
-
-
+
+
+
{/* Table section - will fill remaining space */}
@@ -357,25 +354,25 @@ export default function GroupProposals({
handleRowClick(proposal)}
- className="hover:bg-base-200 text-black dark:text-white rounded-lg cursor-pointer"
+ className="group text-black dark:text-white rounded-lg cursor-pointer"
>
-
+ |
{proposal.id.toString()}
|
-
+ |
{proposal.title}
|
-
+ |
{timeLeft}
|
-
+ |
{proposal.messages.length > 0
? proposal.messages.map((message, index) => (
{getHumanReadableType((message as any)['@type'])}
))
: 'No messages'}
|
-
+ |
{isTalliesLoading ? (
) : (
@@ -388,7 +385,7 @@ export default function GroupProposals({
) : (
- No proposals found
+ No proposal was found.
)}
diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx
index 55e581e..c87bfaa 100644
--- a/components/groups/components/myGroups.tsx
+++ b/components/groups/components/myGroups.tsx
@@ -83,7 +83,7 @@ export function YourGroups({
-
-
-
-
- Group Name |
- Active proposals |
- Authors |
- Group Balance |
- Qualified Majority |
- Group address |
-
-
-
- {isLoading
- ? // Skeleton
- Array(12)
- .fill(0)
- .map((_, index) => (
-
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
- ))
- : // content
- filteredGroups.map((group, index) => (
- 0
- ? proposals[group.policies[0].address]
- : []
- }
- onSelectGroup={(policyAddress, groupName) =>
- handleSelectGroup(
- policyAddress,
- groupName,
- (group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)
- ?.threshold ?? '0'
- )
- }
- />
- ))}
-
-
-
+
+
+
+ Group Name |
+ Active proposals |
+ Authors |
+ Group Balance |
+ Qualified Majority |
+ Group address |
+
+
+
+ {isLoading
+ ? // Skeleton
+ Array(12)
+ .fill(0)
+ .map((_, index) => (
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ |
+
+ ))
+ : // content
+ filteredGroups.map((group, index) => (
+ 0
+ ? proposals[group.policies[0].address]
+ : []
+ }
+ onSelectGroup={(policyAddress, groupName) =>
+ handleSelectGroup(
+ policyAddress,
+ groupName,
+ (group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)
+ ?.threshold ?? '0'
+ )
+ }
+ />
+ ))}
+
+
@@ -226,7 +224,7 @@ function GroupRow({
return (
|
{
e.stopPropagation();
onSelectGroup(
@@ -238,33 +236,35 @@ function GroupRow({
);
}}
>
-
+ |
{truncateString(groupName, 24)}
|
-
+ |
{activeProposals.length > 0 ? (
- {activeProposals.length}
+
+ {activeProposals.length}
+
) : (
'-'
)}
|
-
+ |
{truncateString(
getAuthor(group.ipfsMetadata?.authors) || 'Unknown',
getAuthor(group.ipfsMetadata?.authors || '').startsWith('manifest1') ? 6 : 24
)}
|
-
+ |
{Number(shiftDigits(balance?.amount ?? '0', -6)).toLocaleString(undefined, {
maximumFractionDigits: 6,
})}{' '}
MFX
|
- {`${(group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType).threshold ?? '0'} / ${group.total_weight ?? '0'}`} |
-
+ | {`${(group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType).threshold ?? '0'} / ${group.total_weight ?? '0'}`} |
+
|
diff --git a/components/groups/modals/__tests__/voteDetailsModal.test.tsx b/components/groups/modals/__tests__/voteDetailsModal.test.tsx
new file mode 100644
index 0000000..3035299
--- /dev/null
+++ b/components/groups/modals/__tests__/voteDetailsModal.test.tsx
@@ -0,0 +1,115 @@
+import { describe, test, expect, jest, mock, afterEach } from 'bun:test';
+import React from 'react';
+import { screen, fireEvent, cleanup, waitFor } from '@testing-library/react';
+import VoteDetailsModal from '../voteDetailsModal';
+import {
+ ProposalSDKType,
+ MemberSDKType,
+ VoteSDKType,
+ ProposalStatus,
+ ProposalExecutorResult,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import matchers from '@testing-library/jest-dom/matchers';
+import { renderWithChainProvider } from '@/tests/render';
+import {
+ manifestAddr1,
+ mockGroupFormData,
+ mockMembers,
+ mockProposals,
+ mockTally,
+ mockVotes,
+} from '@/tests/mock';
+
+expect.extend(matchers);
+
+mock.module('react-apexcharts', () => ({
+ default: jest.fn(),
+}));
+
+mock.module('@cosmos-kit/react', () => ({
+ useChain: jest.fn().mockReturnValue({
+ address: mockProposals['test_policy_address'][0].proposers[0],
+ chain: { fees: null },
+ }),
+}));
+
+const mockProposal = mockProposals['test_policy_address'][0];
+
+describe('VoteDetailsModal', () => {
+ const defaultProps = {
+ tallies: mockTally,
+ votes: mockVotes,
+ members: mockMembers,
+ proposal: mockProposal,
+ onClose: jest.fn(),
+ modalId: 'voteDetailsModal',
+ refetchVotes: jest.fn(),
+ refetchTally: jest.fn(),
+ refetchProposals: jest.fn(),
+ };
+
+ afterEach(() => {
+ mock.restore();
+ cleanup();
+ });
+
+ test('renders the component with provided props', () => {
+ renderWithChainProvider(
);
+ expect(screen.getByText(`#${mockProposal.id.toString()}`)).toBeInTheDocument();
+ expect(screen.getByText(mockProposal.title)).toBeInTheDocument();
+ expect(screen.getByText('SUMMARY')).toBeInTheDocument();
+ expect(screen.getByText(mockProposal.summary)).toBeInTheDocument();
+ });
+
+ test('renders the tally chart', () => {
+ renderWithChainProvider(
);
+ expect(screen.getByLabelText('chart-tally')).toBeInTheDocument();
+ });
+
+ test('renders voting countdown timer', () => {
+ renderWithChainProvider(
);
+ expect(screen.getByLabelText('voting-countdown-1')).toBeInTheDocument();
+ expect(screen.getByLabelText('voting-countdown-2')).toBeInTheDocument();
+ });
+
+ test('renders messages section with correct data', () => {
+ renderWithChainProvider(
);
+ expect(screen.getByText('MESSAGES')).toBeInTheDocument();
+ expect(screen.getByText('Send')).toBeInTheDocument();
+ expect(screen.getByText('from_address:')).toBeInTheDocument();
+ expect(screen.getByText('to_address:')).toBeInTheDocument();
+ });
+
+ test('conditionally renders execute button when proposal is accepted', () => {
+ const props = {
+ ...defaultProps,
+ proposal: {
+ ...mockProposal,
+ status: 'PROPOSAL_STATUS_ACCEPTED',
+ executor_result: 'PROPOSAL_EXECUTOR_RESULT_NOT_RUN',
+ },
+ };
+ renderWithChainProvider(
);
+ expect(screen.getByText('Execute')).toBeInTheDocument();
+ });
+
+ test('conditionally renders vote button when proposal is open and user has not voted', () => {
+ renderWithChainProvider(
);
+ expect(screen.getByText('Vote')).toBeInTheDocument();
+ });
+
+ test('does not render vote button when user has already voted', () => {
+ const props = { ...defaultProps, votes: [{ ...mockVotes[0], voter: 'proposer1' }] };
+ renderWithChainProvider(
);
+ const btn = screen.getByLabelText('action-btn');
+ expect(btn.textContent).not.toBe('Vote');
+ });
+
+ test('handles vote button click and opens voting modal', async () => {
+ renderWithChainProvider(
);
+ const voteButton = screen.getByText('Vote');
+ fireEvent.click(voteButton);
+ await waitFor(() => expect(screen.getByLabelText('vote-modal')).toBeInTheDocument());
+ });
+});
diff --git a/components/groups/modals/voteDetailsModal.tsx b/components/groups/modals/voteDetailsModal.tsx
index 140f409..5c9f602 100644
--- a/components/groups/modals/voteDetailsModal.tsx
+++ b/components/groups/modals/voteDetailsModal.tsx
@@ -66,7 +66,7 @@ function VoteDetailsModal({
const { theme } = useTheme();
- const textColor = theme === 'dark' ? '#E0D1D4' : '#2e2e2e';
+ const textColor = theme === 'dark' ? '#FFFFFF' : '#161616';
const normalizedMembers = useMemo(
() =>
@@ -130,6 +130,9 @@ function VoteDetailsModal({
labels: {
useSeriesColors: true,
},
+ markers: {
+ strokeWidth: 0,
+ },
},
states: {
normal: {
@@ -182,7 +185,7 @@ function VoteDetailsModal({
data: chartData,
},
],
- colors: ['#00D515', '#F54562', '#FBBD23', '#3B82F6'],
+ colors: ['#4CAF50', '#E53935', '#FFB300', '#3F51B5'],
tooltip: {
enabled: false,
},
@@ -349,9 +352,9 @@ function VoteDetailsModal({
if (Array.isArray(value)) {
return (
-
{key}:
+
{key}:
{value.map((item, index) => (
-
+
{renderMessageField(`Item ${index + 1}`, item, depth + 1)}
))}
@@ -360,7 +363,7 @@ function VoteDetailsModal({
} else {
return (
-
{key}:
+
{key}:
{Object.entries(value).map(([subKey, subValue]) =>
renderMessageField(subKey, subValue, depth + 1)
)}
@@ -370,11 +373,13 @@ function VoteDetailsModal({
} else {
return (
-
{key}:
+
{key}:
{typeof value === 'string' && value.match(/^[a-zA-Z0-9]{40,}$/) ? (
) : (
-
{truncateText(String(value))}
+
+ {truncateText(String(value))}
+
)}
);
@@ -416,21 +421,25 @@ function VoteDetailsModal({
return (