Repository: liftedinit/manifest-app
Pull request #366: chore: Refactor the Groups page and components (part 1)
Author: login: hansl
Created: 2025-03-13T18:22:21Z | Merged: 2025-03-19T12:44:37Z

Move all the proposals logic to its own component. Simplify Tab management and transitions inside groups. Refactor some DOM (e.g. Loading skeleton) into its own component. Simplify the states.

More to come.


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Confirmation actions now disable during signing to prevent premature input.
  - Enhanced proposal display offers detailed, interactive voting information with seamless navigation.
  - Group management now features improved loading states and a refined wallet connection view, delivering a more responsive experience.
  - Token balance displays include a robust fallback for clearer denomination information.
  - Introduced a new loading component for group rows to enhance user experience.
  
- **Refactor**
  - Navigation and control elements have been restructured for a cleaner, more intuitive group management interface.
  - Wallet connection handling has been modularized for improved readability and maintainability.
  - Tab management has been simplified with a new component for better user interface experience.
  
- **Tests**
  - New test cases added for the `isProposalPassing` function to validate various voting scenarios.
  - Introduced test suite for `GroupControls` to verify tab navigation functionality.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/components/bank/components/historyBox.tsx b/components/bank/components/historyBox.tsx
index 0c4e9a69..e2539b02 100644
--- a/components/bank/components/historyBox.tsx
+++ b/components/bank/components/historyBox.tsx
@@ -65,7 +65,10 @@ export function HistoryBox({
   }
 
   return (
-    <div className="w-full mx-auto  h-full flex flex-col  overflow-x-hidden">
+    <div
+      className="w-full mx-auto  h-full flex flex-col  overflow-x-hidden"
+      data-testid="historyBox"
+    >
       {isLoading ? (
         <div className="flex-1 overflow-hidden h-full">
           <div aria-label="skeleton" className="space-y-2">
diff --git a/components/bank/components/tokenList.tsx b/components/bank/components/tokenList.tsx
index 4cb1e9f1..67788dfa 100644
--- a/components/bank/components/tokenList.tsx
+++ b/components/bank/components/tokenList.tsx
@@ -72,7 +72,7 @@ export const TokenList = React.memo(function TokenList(props: Readonly<TokenList
   );
 
   return (
-    <div className="w-full mx-auto rounded-[24px] h-full flex flex-col">
+    <div className="w-full mx-auto rounded-[24px] h-full flex flex-col" data-testid="tokenList">
       <div className="flex-1 overflow-y-auto">
         {isLoading ? (
           <div className="space-y-2" aria-label="skeleton-loader">
diff --git a/components/factory/components/DenomList.tsx b/components/factory/components/DenomList.tsx
index f7a1c795..0517aa7d 100644
--- a/components/factory/components/DenomList.tsx
+++ b/components/factory/components/DenomList.tsx
@@ -200,7 +200,7 @@ export default function DenomList({
   };
 
   return (
-    <div className="w-full mx-auto rounded-[24px] h-full flex flex-col">
+    <div className="w-full mx-auto rounded-[24px] h-full flex flex-col" data-testid="denomList">
       <div className="flex flex-col gap-4 mb-4">
         <div className="overflow-auto">
           <div className="max-w-8xl mx-auto">
diff --git a/components/factory/forms/ConfirmationForm.tsx b/components/factory/forms/ConfirmationForm.tsx
index 183f1884..7e63c843 100644
--- a/components/factory/forms/ConfirmationForm.tsx
+++ b/components/factory/forms/ConfirmationForm.tsx
@@ -200,6 +200,7 @@ export default function ConfirmationForm({
         <button
           onClick={prevStep}
           className="btn btn-neutral dark:text-white text-black w-[calc(50%-12px)]"
+          disabled={isSigning}
         >
           Back: Token Metadata
         </button>
diff --git a/components/groups/components/Proposals.tsx b/components/groups/components/Proposals.tsx
new file mode 100644
index 00000000..18e070db
--- /dev/null
+++ b/components/groups/components/Proposals.tsx
@@ -0,0 +1,258 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+export function isProposalPassing(tally: TallyResultSDKType | undefined, policyThreshold: number) {
+  const yesCount = BigInt(tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+/**
+ * GroupProposals component displays a list of proposals for a group in a table.
+ * @param group The group to display proposals for.
+ * @param proposals The list of proposals to display.
+ * @constructor
+ */
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  // This is O(n^2), even if we don't expect a lot of proposals, let's memoize it.
+  const proposalTallies = React.useMemo(() => {
+    return proposals.map(proposal => ({
+      proposal,
+      tally: tallies.find(t => t.proposalId === proposal.id)?.tally,
+    }));
+  }, [proposals, tallies]);
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+        data-testid="proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposalTallies.map(({ proposal, tally }) => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group?: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group?.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
+
+  let status = 'Pending';
+  // We need to compare strings here
+  if (
+    proposal.executor_result.toString() ===
+    proposalExecutorResultToJSON(ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE)
+  ) {
+    status = 'Failure';
+  } else if (
+    proposal.status.toString() === proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
+  ) {
+    status = 'Execute';
+  } else if (tally) {
+    const { isPassing, isThresholdReached, isTie } = isProposalPassing(
+      tally?.tally,
+      Number.parseInt(policyThreshold) || 0
+    );
+    if (isThresholdReached) {
+      if (isTie) {
+        status = 'Tie';
+      } else {
+        status = isPassing ? 'Passing' : 'Failing';
+      }
+    }
+  }
+
+  return (
+    <>
+      <tr
+        key={proposal.id.toString()}
+        onClick={() => {
+          setShowVoteModal(true);
+          // Update URL without navigating
+          router.push(
+            {
+              pathname: router.pathname,
+              query: { ...router.query, proposalId: proposal.id.toString() },
+            },
+            undefined,
+            {
+              shallow: true,
+            }
+          );
+        }}
+        className="group text-black dark:text-white rounded-lg cursor-pointer"
+      >
+        <td className="bg-secondary group-hover:bg-base-300 rounded-l-[12px] px-4 py-4 w-[25%]">
+          {proposal.id.toString()}
+        </td>
+        <td
+          className={`bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:rounded-none xxs:rounded-r-[12px] xs:rounded-r-[12px] xl:rounded-r-none`}
+        >
+          {proposal.title}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] hidden xl:table-cell">
+          {timeLeft}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:table-cell md:hidden hidden xl:table-cell ">
+          {proposal.messages.length > 0
+            ? getHumanReadableType((proposal.messages[0] as any)['@type'])
+            : 'No messages'}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 rounded-r-[12px] sm:table-cell xxs:hidden hidden 2xl:table-cell">
+          {!tally ? <span className="loading loading-spinner loading-xs"></span> : status}
+
+          {
+            // Show vote modal if the proposal is selected, cannot be outside `<tr>`.
+            showVoteModal && (
+              <VoteDetailsModal
+                policyAddress={policyAddress}
+                proposalId={proposal.id}
+                onClose={() => {
+                  setShowVoteModal(false);
+                  const { proposalId: _, ...query } = router.query;
+                  router.push({ pathname: router.pathname, query }, undefined, { shallow: true });
+                }}
+                showVoteModal={showVoteModal}
+              />
+            )
+          }
+        </td>
+      </tr>
+    </>
+  );
+};
diff --git a/components/groups/components/__tests__/Proposals.test.tsx b/components/groups/components/__tests__/Proposals.test.tsx
new file mode 100644
index 00000000..6cd91af4
--- /dev/null
+++ b/components/groups/components/__tests__/Proposals.test.tsx
@@ -0,0 +1,99 @@
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import { describe, expect, test } from 'bun:test';
+
+import { isProposalPassing } from '@/components';
+
+describe('isProposalPassing', () => {
+  test('works with a passing vote', () => {
+    const tally: TallyResultSDKType = {
+      yes_count: '10',
+      no_count: '5',
+      abstain_count: '2',
+      no_with_veto_count: '1',
+    };
+
+    const policyThreshold = 10;
+
+    const results = isProposalPassing(tally, policyThreshold);
+    expect(results.isPassing).toBe(true);
+    expect(results.yesCount).toBe(10n);
+    expect(results.noCount).toBe(5n);
+    expect(results.noWithVetoCount).toBe(1n);
+    expect(results.abstainCount).toBe(2n);
+    expect(results.isThresholdReached).toBe(true);
+    expect(results.isTie).toBe(false);
+  });
+
+  test('works without any tally', () => {
+    const policyThreshold = 10;
+    const results = isProposalPassing(undefined, policyThreshold);
+
+    expect(results.isPassing).toBe(false);
+    expect(results.yesCount).toBe(0n);
+    expect(results.noCount).toBe(0n);
+    expect(results.noWithVetoCount).toBe(0n);
+    expect(results.abstainCount).toBe(0n);
+    expect(results.isThresholdReached).toBe(false);
+    expect(results.isTie).toBe(false);
+  });
+
+  test('works without reaching threshold', () => {
+    const tally: TallyResultSDKType = {
+      yes_count: '10',
+      no_count: '5',
+      abstain_count: '2',
+      no_with_veto_count: '1',
+    };
+
+    const policyThreshold = 20;
+
+    const results = isProposalPassing(tally, policyThreshold);
+    expect(results.isPassing).toBe(false);
+    expect(results.yesCount).toBe(10n);
+    expect(results.noCount).toBe(5n);
+    expect(results.noWithVetoCount).toBe(1n);
+    expect(results.abstainCount).toBe(2n);
+    expect(results.isThresholdReached).toBe(false);
+    expect(results.isTie).toBe(false);
+  });
+
+  test('works with a NO vote', () => {
+    const tally: TallyResultSDKType = {
+      yes_count: '5',
+      no_count: '10',
+      abstain_count: '2',
+      no_with_veto_count: '1',
+    };
+
+    const policyThreshold = 10;
+
+    const results = isProposalPassing(tally, policyThreshold);
+    expect(results.isPassing).toBe(false);
+    expect(results.yesCount).toBe(5n);
+    expect(results.noCount).toBe(10n);
+    expect(results.noWithVetoCount).toBe(1n);
+    expect(results.abstainCount).toBe(2n);
+    expect(results.isThresholdReached).toBe(true);
+    expect(results.isTie).toBe(false);
+  });
+
+  test('works with a tie', () => {
+    const tally: TallyResultSDKType = {
+      yes_count: '5',
+      no_count: '4',
+      abstain_count: '2',
+      no_with_veto_count: '1',
+    };
+
+    const policyThreshold = 10;
+
+    const results = isProposalPassing(tally, policyThreshold);
+    expect(results.isPassing).toBe(false);
+    expect(results.yesCount).toBe(5n);
+    expect(results.noCount).toBe(4n);
+    expect(results.noWithVetoCount).toBe(1n);
+    expect(results.abstainCount).toBe(2n);
+    expect(results.isThresholdReached).toBe(true);
+    expect(results.isTie).toBe(true);
+  });
+});
diff --git a/components/groups/components/__tests__/groupControls.test.tsx b/components/groups/components/__tests__/groupControls.test.tsx
new file mode 100644
index 00000000..d3d2d56d
--- /dev/null
+++ b/components/groups/components/__tests__/groupControls.test.tsx
@@ -0,0 +1,138 @@
+import { ProposalSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { cleanup, fireEvent, screen } from '@testing-library/react';
+import { afterEach, describe, expect, jest, test } from 'bun:test';
+import React from 'react';
+
+import { GroupControls } from '@/components/groups/components/groupControls';
+import {
+  clearAllMocks,
+  formatComponent,
+  mockModule,
+  mockRouter,
+  renderWithChainProvider,
+} from '@/tests';
+
+const mockProps = {
+  group: {
+    policies: [
+      {
+        address: 'test_policy_address',
+        title: 'Test Policy',
+        description: 'Test Policy Description',
+      } as any,
+    ],
+  } as any,
+  isLoading: false,
+  txLoading: false,
+  onBack: jest.fn(),
+  currentPage: 0,
+  setCurrentPage: jest.fn(),
+  totalPages: 1,
+  sendTxs: [],
+  isError: false,
+  balances: [],
+  denoms: [],
+  pageSize: 6,
+  skeletonGroupCount: 0,
+  skeletonTxCount: 0,
+};
+
+const mockProposal: ProposalSDKType = {
+  executor_result: 2,
+  final_tally_result: {
+    yes_count: '1',
+    no_count: '2',
+    abstain_count: '3',
+    no_with_veto_count: '4',
+  },
+  group_policy_address: 'address1',
+  group_policy_version: 0n,
+  group_version: 0n,
+  id: 0n,
+  messages: [],
+  metadata: '',
+  proposers: [],
+  status: 2,
+  submit_time: new Date(),
+  summary: '',
+  title: '',
+  voting_period_end: new Date(),
+};
+
+describe('Tabs functionality', () => {
+  afterEach(() => {
+    cleanup();
+    clearAllMocks();
+  });
+
+  test('renders all tab options and switches between tabs correctly', async () => {
+    mockRouter();
+    mockModule.force('@/hooks/useQueries', () => ({
+      useProposalsByPolicyAccount: jest.fn().mockReturnValue({
+        proposals: [mockProposal],
+        isProposalsLoading: false,
+        isProposalsError: false,
+        refetchProposals: jest.fn(),
+      }),
+    }));
+
+    const mockup = renderWithChainProvider(<GroupControls {...mockProps} />);
+
+    // Ensure all tabs are present in the DOM
+    const proposalTab = screen.getByRole('tab', { name: /proposals/i });
+    const assetsTab = screen.getByRole('tab', { name: /assets/i });
+    const activityTab = screen.getByRole('tab', { name: /activity/i });
+    const tokensTab = screen.getByRole('tab', { name: /tokens/i });
+
+    expect(proposalTab).toBeInTheDocument();
+    expect(assetsTab).toBeInTheDocument();
+    expect(activityTab).toBeInTheDocument();
+    expect(tokensTab).toBeInTheDocument();
+
+    // Ensure the first tab is selected by default
+    expect(screen.queryByTestId('proposals')).toBeInTheDocument();
+
+    // Switch to "Assets" tab and verify the change
+    fireEvent.click(assetsTab);
+    expect(screen.queryByTestId('Group proposals')).not.toBeInTheDocument();
+    expect(screen.queryByTestId('tokenList')).toBeInTheDocument();
+
+    // Switch to "Activity" tab and verify the change
+    fireEvent.click(activityTab);
+    expect(screen.queryByTestId('tokenList')).not.toBeInTheDocument();
+    expect(screen.queryByTestId('historyBox')).toBeInTheDocument();
+
+    // Switch to "Tokens" tab and verify the change
+    fireEvent.click(tokensTab);
+    expect(screen.queryByTestId('historyBox')).not.toBeInTheDocument();
+    expect(screen.queryByTestId('denomList')).toBeInTheDocument();
+  });
+
+  test('routes properly', async () => {
+    mockRouter({
+      query: { tab: 'activity' },
+    });
+    mockModule.force('@/hooks/useQueries', () => ({
+      useProposalsByPolicyAccount: jest.fn().mockReturnValue({
+        proposals: [mockProposal],
+        isProposalsLoading: false,
+        isProposalsError: false,
+        refetchProposals: jest.fn(),
+      }),
+    }));
+
+    renderWithChainProvider(<GroupControls {...mockProps} />);
+    expect(screen.queryByTestId('historyBox')).toBeInTheDocument();
+
+    const assetsTab = screen.getByRole('tab', { name: /assets/i });
+    fireEvent.click(assetsTab);
+    expect(screen.queryByTestId('tokenList')).toBeInTheDocument();
+
+    const router = require('next/router').useRouter();
+    expect(router.push).toHaveBeenCalledWith(
+      expect.not.objectContaining({ pathname: 'unused', query: { tab: 'assets' } }),
+      undefined,
+      { shallow: true }
+    );
+  });
+});
diff --git a/components/groups/components/__tests__/groupProposals.test.tsx b/components/groups/components/__tests__/groupProposals.test.tsx
index 17e816a6..059093e5 100644
--- a/components/groups/components/__tests__/groupProposals.test.tsx
+++ b/components/groups/components/__tests__/groupProposals.test.tsx
@@ -1,14 +1,36 @@
-import { cleanup, fireEvent, screen, waitFor } from '@testing-library/react';
-import { afterEach, beforeEach, describe, expect, jest, mock, test } from 'bun:test';
+import { cleanup, screen } from '@testing-library/react';
+import { afterEach, beforeEach, describe, expect, jest, test } from 'bun:test';
 import React from 'react';
 
-import GroupProposals from '@/components/groups/components/groupControls';
+import { GroupControls } from '@/components/groups/components';
 import { clearAllMocks, mockModule, mockRouter } from '@/tests';
 import { mockGroup, mockGroup2, mockProposals } from '@/tests/data';
 import { renderWithChainProvider } from '@/tests/render';
 
 const mockProps = {
-  policyAddress: 'test_policy_address',
+  group: {
+    policies: [
+      {
+        address: 'test_policy_address',
+        title: 'Test Policy',
+        description: 'Test Policy Description',
+      } as any,
+    ],
+  } as any,
+  proposals: [],
+  isLoading: false,
+  txLoading: false,
+  onBack: jest.fn(),
+  currentPage: 0,
+  setCurrentPage: jest.fn(),
+  totalPages: 1,
+  sendTxs: [],
+  isError: false,
+  balances: [],
+  denoms: [],
+  pageSize: 6,
+  skeletonGroupCount: 0,
+  skeletonTxCount: 0,
 };
 
 describe('ProposalsForPolicy Component', () => {
@@ -71,7 +93,7 @@ describe('ProposalsForPolicy Component', () => {
         refetchProposals: jest.fn(),
       }),
     }));
-    renderWithChainProvider(<GroupProposals {...mockProps} />);
+    renderWithChainProvider(<GroupControls {...mockProps} />);
     expect(screen.getByRole('status')).toBeInTheDocument();
   });
 
@@ -84,7 +106,7 @@ describe('ProposalsForPolicy Component', () => {
         refetchProposals: jest.fn(),
       }),
     }));
-    renderWithChainProvider(<GroupProposals {...mockProps} />);
+    renderWithChainProvider(<GroupControls {...mockProps} />);
     expect(screen.getByText('Error loading proposals')).toBeInTheDocument();
   });
 
@@ -97,7 +119,7 @@ describe('ProposalsForPolicy Component', () => {
         refetchProposals: jest.fn(),
       }),
     }));
-    renderWithChainProvider(<GroupProposals {...mockProps} />);
+    renderWithChainProvider(<GroupControls {...mockProps} />);
     expect(screen.getByText('No proposal was found.')).toBeInTheDocument();
   });
 
@@ -110,7 +132,7 @@ describe('ProposalsForPolicy Component', () => {
         refetchProposals: jest.fn(),
       }),
     }));
-    renderWithChainProvider(<GroupProposals {...mockProps} />);
+    renderWithChainProvider(<GroupControls {...mockProps} />);
     expect(screen.getByText('Proposals')).toBeInTheDocument();
     const proposals = mockProposals['test_policy_address'];
     proposals.forEach(proposal => {
diff --git a/components/groups/components/__tests__/myGroups.test.tsx b/components/groups/components/__tests__/myGroups.test.tsx
index 1551d154..3eabc66f 100644
--- a/components/groups/components/__tests__/myGroups.test.tsx
+++ b/components/groups/components/__tests__/myGroups.test.tsx
@@ -2,7 +2,7 @@ import { cleanup, fireEvent, screen } from '@testing-library/react';
 import { afterEach, beforeEach, describe, expect, jest, mock, test } from 'bun:test';
 import React from 'react';
 
-import YourGroups from '@/components/groups/components/myGroups';
+import { YourGroups } from '@/components';
 import { clearAllMocks, mockModule, mockRouter } from '@/tests';
 import { mockGroup, mockGroup2 } from '@/tests/data';
 import { renderWithChainProvider } from '@/tests/render';
@@ -18,7 +18,7 @@ const mockProps = {
         admin: 'admin1',
         members: [{ member: { address: 'member1' } }],
         total_weight: '1',
-      },
+      } as any,
     ],
   },
   proposals: { policy1: [] },
@@ -29,14 +29,17 @@ const mockPropsWithManyGroups = {
   groups: {
     groups: Array(12)
       .fill(null)
-      .map((_, index) => ({
-        id: `${index + 1}`,
-        metadata: `{"title":"title${index + 1}","authors":"author${index + 1}","summary":"summary${index + 1}","details":"details${index + 1}"}`,
-        policies: [{ address: `policy${index + 1}`, decision_policy: { threshold: '1' } }],
-        admin: `admin${index + 1}`,
-        members: [{ member: { address: `member${index + 1}` } }],
-        total_weight: '1',
-      })),
+      .map(
+        (_, index) =>
+          ({
+            id: `${index + 1}`,
+            metadata: `{"title":"title${index + 1}","authors":"author${index + 1}","summary":"summary${index + 1}","details":"details${index + 1}"}`,
+            policies: [{ address: `policy${index + 1}`, decision_policy: { threshold: '1' } }],
+            admin: `admin${index + 1}`,
+            members: [{ member: { address: `member${index + 1}` } }],
+            total_weight: '1',
+          }) as any
+      ),
   },
   proposals: {},
   isLoading: false,
@@ -115,7 +118,7 @@ describe('Groups Component', () => {
     // Verify that router.push was called with the correct arguments
     const router = require('next/router').useRouter();
     expect(router.push).toHaveBeenCalledWith(
-      expect.stringContaining('/groups?policyAddress=policy1'),
+      expect.objectContaining({ query: { policyAddress: 'policy1' } }),
       undefined,
       { shallow: true }
     );
diff --git a/components/groups/components/groupControls.tsx b/components/groups/components/groupControls.tsx
index 41e7b828..799b98ce 100644
--- a/components/groups/components/groupControls.tsx
+++ b/components/groups/components/groupControls.tsx
@@ -1,34 +1,29 @@
 import { useChain } from '@cosmos-kit/react';
-import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import { Tab } from '@headlessui/react';
 import {
   ProposalSDKType,
   ProposalStatus,
   proposalStatusToJSON,
 } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
-import {
-  ProposalExecutorResult,
-  proposalExecutorResultToJSON,
-} from 'cosmjs-types/cosmos/group/v1/types';
 import { useRouter } from 'next/router';
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
 
 import { HistoryBox, TokenList } from '@/components';
 import { TxMessage } from '@/components/bank/types';
 import DenomList from '@/components/factory/components/DenomList';
-import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { GroupProposals } from '@/components/groups/components/Proposals';
 import { ArrowRightIcon } from '@/components/icons';
 import env from '@/config/env';
 import useIsMobile from '@/hooks/useIsMobile';
-import { useMultipleTallyCounts, useProposalsByPolicyAccount } from '@/hooks/useQueries';
+import { ExtendedGroupType, useProposalsByPolicyAccount } from '@/hooks/useQueries';
 import { useResponsivePageSize } from '@/hooks/useResponsivePageSize';
+import { group as groupSchema } from '@/schemas';
 import { CombinedBalanceInfo, ExtendedMetadataSDKType } from '@/utils';
 import { ProfileAvatar } from '@/utils/identicon';
 
-type GroupControlsProps = {
-  policyAddress: string;
-  groupName: string;
+export type GroupControlsProps = {
+  group: ExtendedGroupType;
   onBack: () => void;
-  policyThreshold: string;
   isLoading: boolean;
   currentPage: number;
   setCurrentPage: React.Dispatch<React.SetStateAction<number>>;
@@ -44,11 +39,11 @@ type GroupControlsProps = {
   proposalPageSize?: number;
 };
 
-export default function GroupControls({
-  policyAddress,
-  groupName,
+const TAB_SLUGS = ['proposals', 'assets', 'activity', 'tokens'];
+
+export function GroupControls({
+  group,
   onBack,
-  policyThreshold,
   isLoading,
   currentPage,
   setCurrentPage,
@@ -63,15 +58,13 @@ export default function GroupControls({
   skeletonTxCount,
   proposalPageSize = 10,
 }: GroupControlsProps) {
+  const policyAddress = group?.policies?.[0]?.address ?? '';
+
   const isMobile = useIsMobile();
   const { proposals, isProposalsLoading, isProposalsError } =
     useProposalsByPolicyAccount(policyAddress);
 
-  const [selectedProposalId, setSelectedProposalId] = useState<bigint | undefined>(undefined);
-  const [showVoteModal, setShowVoteModal] = useState(false);
-  const [activeTab, setActiveTab] = useState('proposals');
-
-  const [searchTerm, setSearchTerm] = useState('');
+  const [activeTab, setActiveTabInner] = useState(0);
 
   // We need to compare strings here
   const filterProposals = (proposals: ProposalSDKType[]) => {
@@ -86,103 +79,38 @@ export default function GroupControls({
 
   const router = useRouter();
 
+  const setActiveTab = useCallback(
+    (tab: number) => {
+      setActiveTabInner(tab);
+      router.push(
+        {
+          pathname: router.pathname,
+          query: {
+            ...router.query,
+            tab: TAB_SLUGS[tab],
+          },
+        },
+        undefined,
+        { shallow: true }
+      );
+    },
+    [router]
+  );
+
   useEffect(() => {
     const { tab } = router.query;
-    if (tab && typeof tab === 'string') {
-      setActiveTab(tab);
-    }
+    const maybeIndex = TAB_SLUGS.indexOf(tab as string);
+    setActiveTabInner(maybeIndex === -1 ? 0 : maybeIndex);
   }, [router.query]);
 
-  const handleTabClick = (tab: string) => {
-    setActiveTab(tab);
-    router.push(`/groups?policyAddress=${policyAddress}&tab=${tab}`, undefined, { shallow: true });
-  };
-
-  useEffect(() => {
-    const { proposalId } = router.query;
-    if (proposalId && typeof proposalId === 'string' && proposals.length > 0) {
-      const proposalToOpen = proposals.find(p => p.id.toString() === proposalId);
-      if (proposalToOpen) {
-        setSelectedProposalId(proposalToOpen.id);
-        setShowVoteModal(true);
-      } else {
-        console.warn(`Proposal with ID ${proposalId} not found`);
-        // remove the invalid proposalId from the URL
-        router.push(`/groups?policyAddress=${policyAddress}`, undefined, { shallow: true });
-      }
-    }
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [router.query, proposals, policyAddress]);
-
-  const handleRowClick = (proposal: ProposalSDKType) => {
-    setSelectedProposalId(proposal.id);
-    setShowVoteModal(true);
-    // Update URL without navigating
-    router.push(`/groups?policyAddress=${policyAddress}&proposalId=${proposal.id}`, undefined, {
-      shallow: true,
-    });
-  };
-
-  const handleCloseVoteModal = () => {
-    setSelectedProposalId(undefined);
-    setShowVoteModal(false);
-    // Remove proposalId from URL when closing the modal
-    router.push(`/groups?policyAddress=${policyAddress}`, undefined, { shallow: true });
-  };
-
-  function isProposalPassing(tally: QueryTallyResultResponseSDKType) {
-    const yesCount = BigInt(tally?.tally?.yes_count ?? '0');
-    const noCount = BigInt(tally?.tally?.no_count ?? '0');
-    const noWithVetoCount = BigInt(tally?.tally?.no_with_veto_count ?? '0');
-    const abstainCount = BigInt(tally?.tally?.abstain_count ?? '0');
-
-    const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
-    const totalNoVotes = noCount + noWithVetoCount;
-
-    // Check if threshold is reached
-    const threshold = BigInt(policyThreshold);
-    const isThresholdReached = totalVotes >= threshold;
-
-    // Check for tie
-    const isTie = yesCount === totalNoVotes && yesCount > 0;
-
-    // Determine if passing based on vote distribution
-    const isPassing = isThresholdReached && yesCount > totalNoVotes;
-
-    return {
-      isPassing,
-      yesCount,
-      noCount,
-      noWithVetoCount,
-      abstainCount,
-      isThresholdReached,
-      isTie,
-    };
-  }
-
-  function getHumanReadableType(type: string): string {
-    const parts = type.split('.');
-    const lastPart = parts[parts.length - 1];
-    return lastPart
-      .replace('Msg', '')
-      .replace(/([A-Z])/g, ' $1')
-      .trim();
-  }
+  const groupName = useMemo(() => {
+    const metadata = groupSchema.metadataFromJson(group.metadata, false);
+    return metadata?.title ?? 'Untitled Group';
+  }, [group.metadata]);
 
   const { address } = useChain(env.chain);
 
-  const filteredProposals = filterProposals(proposals).filter(proposal =>
-    proposal.title.toLowerCase().includes(searchTerm.toLowerCase())
-  );
-
-  const { tallies, isLoading: isTalliesLoading } = useMultipleTallyCounts(proposals.map(p => p.id));
-
-  const handleKeyDown = (e: React.KeyboardEvent, tab: string) => {
-    if (e.key === 'Enter' || e.key === ' ') {
-      e.preventDefault();
-      handleTabClick(tab);
-    }
-  };
+  const filteredProposals = filterProposals(proposals);
 
   const [proposalCurrentPage, setProposalCurrentPage] = useState(1);
 
@@ -225,12 +153,6 @@ export default function GroupControls({
     Math.ceil(filteredProposals.length / proposalPageSizes.proposals)
   );
 
-  // Get current page proposals
-  const currentProposals = filteredProposals.slice(
-    (proposalCurrentPage - 1) * proposalPageSizes.proposals,
-    proposalCurrentPage * proposalPageSizes.proposals
-  );
-
   useEffect(() => {
     // Adjust the current page if the number of proposals changes
     if (proposalCurrentPage > totalProposalPages) {
@@ -260,66 +182,25 @@ export default function GroupControls({
         </div>
       </div>
 
-      <div role="tablist" className="tabs tabs-border tabs-md md:tabs-lg flex flex-row">
-        <button
-          role="tab"
-          id="proposals-tab"
-          aria-selected={activeTab === 'proposals'}
-          aria-controls="proposals-panel"
-          tabIndex={activeTab === 'proposals' ? 0 : -1}
-          className={`font-bold tab ${activeTab === 'proposals' ? 'tab-active' : ''}`}
-          onClick={() => handleTabClick('proposals')}
-          onKeyDown={e => handleKeyDown(e, 'proposals')}
-        >
-          Proposals
-        </button>
-        <button
-          role="tab"
-          id="assets-tab"
-          aria-selected={activeTab === 'assets'}
-          aria-controls="assets-panel"
-          tabIndex={activeTab === 'assets' ? 0 : -1}
-          className={`font-bold tab ${activeTab === 'assets' ? 'tab-active' : ''}`}
-          onClick={() => handleTabClick('assets')}
-          onKeyDown={e => handleKeyDown(e, 'assets')}
-        >
-          Assets
-        </button>
-        <button
-          role="tab"
-          id="history-tab"
-          aria-selected={activeTab === 'history'}
-          aria-controls="history-panel"
-          tabIndex={activeTab === 'history' ? 0 : -1}
-          className={`font-bold tab ${activeTab === 'history' ? 'tab-active' : ''}`}
-          onClick={() => handleTabClick('history')}
-          onKeyDown={e => handleKeyDown(e, 'history')}
-        >
-          Activity
-        </button>
-        <button
-          role="tab"
-          id="tokens-tab"
-          aria-selected={activeTab === 'tokens'}
-          aria-controls="tokens-panel"
-          tabIndex={activeTab === 'tokens' ? 0 : -1}
-          className={`font-bold tab ${activeTab === 'tokens' ? 'tab-active' : ''}`}
-          onClick={() => handleTabClick('tokens')}
-          onKeyDown={e => handleKeyDown(e, 'tokens')}
-        >
-          Tokens
-        </button>
-      </div>
-
-      <div className="flex flex-col w-full mt-4">
-        <div
-          id="proposals-panel"
-          role="tabpanel"
-          aria-labelledby="proposals-tab"
-          hidden={activeTab !== 'proposals'}
-        >
-          {activeTab === 'proposals' &&
-            (isProposalsLoading ? (
+      <Tab.Group selectedIndex={activeTab} onChange={tab => setActiveTab(tab)}>
+        <Tab.List className="flex flex-row tabs tabs-border tabs-lg">
+          <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
+            Proposals
+          </Tab>
+          <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
+            Assets
+          </Tab>
+          <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
+            Activity
+          </Tab>
+          <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
+            Tokens
+          </Tab>
+        </Tab.List>
+
+        <Tab.Panels className="mt-4">
+          <Tab.Panel>
+            {isProposalsLoading ? (
               <div
                 className="flex justify-center items-center h-64"
                 role="status"
@@ -332,200 +213,80 @@ export default function GroupControls({
                 Error loading proposals
               </div>
             ) : filteredProposals.length > 0 ? (
-              <table
-                className="table w-full border-separate border-spacing-y-3 -mt-6"
-                aria-label="Group proposals"
-              >
-                <thead>
-                  <tr className="text-sm font-medium">
-                    <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
-                      ID
-                    </th>
-                    <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
-                      Title
-                    </th>
-                    <th
-                      className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell"
-                      scope="col"
-                    >
-                      Time Left
-                    </th>
-                    <th
-                      className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
-                      scope="col"
-                    >
-                      Type
-                    </th>
-                    <th
-                      className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
-                      scope="col"
-                    >
-                      Status
-                    </th>
-                  </tr>
-                </thead>
-                <tbody className="space-y-4">
-                  {currentProposals.map(proposal => {
-                    const endTime = new Date(proposal?.voting_period_end);
-                    const now = new Date();
-                    const msPerMinute = 1000 * 60;
-                    const msPerHour = msPerMinute * 60;
-                    const msPerDay = msPerHour * 24;
-
-                    const diff = endTime.getTime() - now.getTime();
-
-                    let timeLeft: string;
-
-                    if (diff <= 0) {
-                      timeLeft = 'none';
-                    } else if (diff >= msPerDay) {
-                      const days = Math.floor(diff / msPerDay);
-                      timeLeft = `${days} day${days === 1 ? '' : 's'}`;
-                    } else if (diff >= msPerHour) {
-                      const hours = Math.floor(diff / msPerHour);
-                      timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
-                    } else if (diff >= msPerMinute) {
-                      const minutes = Math.floor(diff / msPerMinute);
-                      timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
-                    } else {
-                      timeLeft = 'less than a minute';
-                    }
-
-                    const proposalTally = tallies.find(t => t.proposalId === proposal.id)?.tally;
-
-                    let status = 'Pending';
-                    // We need to compare strings here
-                    if (
-                      proposal.executor_result.toString() ===
-                      proposalExecutorResultToJSON(
-                        ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE
-                      )
-                    ) {
-                      status = 'Failure';
-                    } else if (
-                      proposal.status.toString() ===
-                      proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
-                    ) {
-                      status = 'Execute';
-                    } else if (proposalTally) {
-                      const { isPassing, isThresholdReached, isTie } =
-                        isProposalPassing(proposalTally);
-                      if (isThresholdReached) {
-                        if (isTie) {
-                          status = 'Tie';
-                        } else {
-                          status = isPassing ? 'Passing' : 'Failing';
-                        }
-                      }
-                    }
-
-                    return (
-                      <tr
-                        key={proposal.id.toString()}
-                        onClick={() => handleRowClick(proposal)}
-                        className="group text-black dark:text-white rounded-lg cursor-pointer"
-                      >
-                        <td className="bg-secondary group-hover:bg-base-300 rounded-l-[12px] px-4 py-4 w-[25%]">
-                          {proposal.id.toString()}
-                        </td>
-                        <td
-                          className={`bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:rounded-none xxs:rounded-r-[12px] xs:rounded-r-[12px] xl:rounded-r-none`}
-                        >
-                          {proposal.title}
-                        </td>
-                        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] hidden xl:table-cell">
-                          {timeLeft}
-                        </td>
-                        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:table-cell md:hidden hidden xl:table-cell ">
-                          {proposal.messages.length > 0
-                            ? getHumanReadableType((proposal.messages[0] as any)['@type'])
-                            : 'No messages'}
-                        </td>
-                        <td className="bg-secondary group-hover:bg-base-300 rounded-r-[12px] sm:table-cell xxs:hidden hidden 2xl:table-cell">
-                          {isTalliesLoading ? (
-                            <span className="loading loading-spinner loading-xs"></span>
-                          ) : (
-                            status
-                          )}
-                        </td>
-                      </tr>
-                    );
-                  })}
-                </tbody>
-              </table>
+              <GroupProposals
+                group={group}
+                proposals={filteredProposals.slice(
+                  (proposalCurrentPage - 1) * proposalPageSizes.proposals,
+                  proposalCurrentPage * proposalPageSizes.proposals
+                )}
+              />
             ) : (
               <div className="text-center py-8 text-gray-500" role="status">
                 No proposal was found.
               </div>
-            ))}
-          {totalProposalPages > 1 && (
-            <div className="flex items-center justify-end gap-2 mt-4">
-              <button
-                onClick={() => setProposalCurrentPage(prev => Math.max(1, prev - 1))}
-                disabled={proposalCurrentPage === 1}
-                aria-label="Previous page"
-                className="p-2 hover:bg-[#0000001A] dark:hover:bg-[#FFFFFF1A] text-black dark:text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
-              >
-                ‹
-              </button>
-
-              {[...Array(totalProposalPages)].map((_, index) => {
-                const pageNum = index + 1;
-                if (
-                  pageNum === 1 ||
-                  pageNum === totalProposalPages ||
-                  (pageNum >= proposalCurrentPage - 1 && pageNum <= proposalCurrentPage + 1)
-                ) {
-                  return (
-                    <button
-                      key={pageNum}
-                      onClick={() => setProposalCurrentPage(pageNum)}
-                      aria-label={`Page ${pageNum}`}
-                      aria-current={proposalCurrentPage === pageNum ? 'page' : undefined}
-                      className={`w-8 h-8 flex items-center justify-center rounded-lg transition-colors  
+            )}
+            {totalProposalPages > 1 && (
+              <div className="flex items-center justify-end gap-2 mt-4">
+                <button
+                  onClick={() => setProposalCurrentPage(prev => Math.max(1, prev - 1))}
+                  disabled={proposalCurrentPage === 1}
+                  aria-label="Previous page"
+                  className="p-2 hover:bg-[#0000001A] dark:hover:bg-[#FFFFFF1A] text-black dark:text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+                >
+                  ‹
+                </button>
+
+                {[...Array(totalProposalPages)].map((_, index) => {
+                  const pageNum = index + 1;
+                  if (
+                    pageNum === 1 ||
+                    pageNum === totalProposalPages ||
+                    (pageNum >= proposalCurrentPage - 1 && pageNum <= proposalCurrentPage + 1)
+                  ) {
+                    return (
+                      <button
+                        key={pageNum}
+                        onClick={() => setProposalCurrentPage(pageNum)}
+                        aria-label={`Page ${pageNum}`}
+                        aria-current={proposalCurrentPage === pageNum ? 'page' : undefined}
+                        className={`w-8 h-8 flex items-center justify-center rounded-lg transition-colors  
                         ${
                           proposalCurrentPage === pageNum
                             ? 'bg-[#0000001A] dark:bg-[#FFFFFF1A] text-black dark:text-white'
                             : 'hover:bg-[#0000001A] dark:hover:bg-[#FFFFFF1A] text-black dark:text-white'
                         }`}
-                    >
-                      {pageNum}
-                    </button>
-                  );
-                } else if (
-                  pageNum === proposalCurrentPage - 2 ||
-                  pageNum === proposalCurrentPage + 2
-                ) {
-                  return (
-                    <span key={pageNum} className="text-black dark:text-white">
-                      ...
-                    </span>
-                  );
-                }
-                return null;
-              })}
-
-              <button
-                onClick={() =>
-                  setProposalCurrentPage(prev => Math.min(totalProposalPages, prev + 1))
-                }
-                disabled={proposalCurrentPage === totalProposalPages}
-                aria-label="Next page"
-                className="p-2 hover:bg-[#0000001A] dark:hover:bg-[#FFFFFF1A] text-black dark:text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
-              >
-                ›
-              </button>
-            </div>
-          )}
-        </div>
+                      >
+                        {pageNum}
+                      </button>
+                    );
+                  } else if (
+                    pageNum === proposalCurrentPage - 2 ||
+                    pageNum === proposalCurrentPage + 2
+                  ) {
+                    return (
+                      <span key={pageNum} className="text-black dark:text-white">
+                        ...
+                      </span>
+                    );
+                  }
+                  return null;
+                })}
+
+                <button
+                  onClick={() =>
+                    setProposalCurrentPage(prev => Math.min(totalProposalPages, prev + 1))
+                  }
+                  disabled={proposalCurrentPage === totalProposalPages}
+                  aria-label="Next page"
+                  className="p-2 hover:bg-[#0000001A] dark:hover:bg-[#FFFFFF1A] text-black dark:text-white rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
+                >
+                  ›
+                </button>
+              </div>
+            )}
+          </Tab.Panel>
 
-        <div
-          id="assets-panel"
-          role="tabpanel"
-          aria-labelledby="assets-tab"
-          hidden={activeTab !== 'assets'}
-        >
-          {activeTab === 'assets' && (
+          <Tab.Panel>
             <TokenList
               balances={balances}
               isLoading={isLoading}
@@ -534,16 +295,9 @@ export default function GroupControls({
               isGroup={true}
               admin={policyAddress}
             />
-          )}
-        </div>
+          </Tab.Panel>
 
-        <div
-          id="history-panel"
-          role="tabpanel"
-          aria-labelledby="history-tab"
-          hidden={activeTab !== 'history'}
-        >
-          {activeTab === 'history' && (
+          <Tab.Panel>
             <HistoryBox
               isLoading={isLoading}
               address={policyAddress}
@@ -556,38 +310,20 @@ export default function GroupControls({
               skeletonGroupCount={skeletonGroupCount}
               skeletonTxCount={skeletonTxCount}
             />
-          )}
-        </div>
+          </Tab.Panel>
 
-        <div
-          id="tokens-panel"
-          role="tabpanel"
-          aria-labelledby="tokens-tab"
-          hidden={activeTab !== 'tokens'}
-        >
-          {activeTab === 'tokens' && (
-            <div className="h-full w-full -mt-7">
-              <DenomList
-                denoms={denoms}
-                isLoading={isLoading}
-                address={address ?? ''}
-                admin={policyAddress}
-                pageSize={pageSize - 1}
-                isGroup={true}
-              />
-            </div>
-          )}
-        </div>
-      </div>
-      {selectedProposalId !== undefined && (
-        <VoteDetailsModal
-          policyAddress={policyAddress}
-          proposals={proposals}
-          proposalId={selectedProposalId}
-          onClose={handleCloseVoteModal}
-          showVoteModal={showVoteModal}
-        />
-      )}
+          <Tab.Panel className="-mt-7">
+            <DenomList
+              denoms={denoms}
+              isLoading={isLoading}
+              address={address ?? ''}
+              admin={policyAddress}
+              pageSize={pageSize - 1}
+              isGroup={true}
+            />
+          </Tab.Panel>
+        </Tab.Panels>
+      </Tab.Group>
     </div>
   );
 }
diff --git a/components/groups/components/index.tsx b/components/groups/components/index.tsx
index 23338717..fb0f1cfe 100644
--- a/components/groups/components/index.tsx
+++ b/components/groups/components/index.tsx
@@ -2,4 +2,5 @@ export * from './CountdownTimer';
 export * from '../modals/groupInfo';
 export * from './groupControls';
 export * from './myGroups';
+export * from './Proposals';
 export * from '../../react/StepIndicator';
diff --git a/components/groups/components/myGroups.tsx b/components/groups/components/myGroups.tsx
index d3e02fe2..11a5f410 100644
--- a/components/groups/components/myGroups.tsx
+++ b/components/groups/components/myGroups.tsx
@@ -9,7 +9,8 @@ import { useRouter } from 'next/router';
 import React, { useEffect, useMemo, useState } from 'react';
 import { PiInfo } from 'react-icons/pi';
 
-import { GroupInfo, MemberManagementModal } from '@/components';
+import { GroupInfo, MemberManagementModal, TokenBalance } from '@/components';
+import { GroupControls } from '@/components';
 import { MemberIcon, SearchIcon } from '@/components/icons';
 import { TruncatedAddressWithCopy } from '@/components/react/addressCopy';
 import env from '@/config/env';
@@ -26,21 +27,17 @@ import {
   useTotalSupply,
 } from '@/hooks/useQueries';
 import { useResponsivePageSize } from '@/hooks/useResponsivePageSize';
-import { group as groupSchema } from '@/schemas';
 import {
   CombinedBalanceInfo,
   ExtendedMetadataSDKType,
   MFX_TOKEN_BASE,
   MFX_TOKEN_DATA,
   denomToAsset,
-  shiftDigits,
   truncateString,
   unsafeConvertTokenBase,
 } from '@/utils';
 import { ProfileAvatar } from '@/utils/identicon';
 
-import GroupControls from './groupControls';
-
 // Add this interface outside the component
 interface PageSizeConfig {
   groupInfo: number;
@@ -49,7 +46,7 @@ interface PageSizeConfig {
   skeleton: number;
 }
 
-export default React.memo(function YourGroups({
+export const YourGroups = ({
   groups,
   proposals,
   isLoading,
@@ -57,7 +54,7 @@ export default React.memo(function YourGroups({
   groups: ExtendedQueryGroupsByMemberResponseSDKType;
   proposals: { [policyAddress: string]: ProposalSDKType[] };
   isLoading: boolean;
-}) {
+}) => {
   const [searchTerm, setSearchTerm] = useState('');
   const [currentPage, setCurrentPage] = useState(1);
   const isMobile = useIsMobile();
@@ -103,7 +100,6 @@ export default React.memo(function YourGroups({
   const skeletonTxCount = pageSize.skeleton;
 
   const [selectedGroup, setSelectedGroup] = useState<ExtendedGroupType | null>(null);
-  const [selectedGroupName, setSelectedGroupName] = useState<string>('Untitled Group');
 
   const router = useRouter();
   const { address } = useChain(env.chain);
@@ -132,24 +128,16 @@ export default React.memo(function YourGroups({
     if (policyAddress && typeof policyAddress === 'string') {
       const group = groups.groups.find(g => g.policies?.[0]?.address === policyAddress);
       if (group) {
-        let groupName = 'Untitled Group';
-        try {
-          const metadata = group.metadata ? JSON.parse(group.metadata) : null;
-          groupName = metadata?.title ?? 'Untitled Group';
-        } catch (e) {
-          // If JSON parsing fails, fall back to default name
-          // console.warn('Failed to parse group metadata:', e);
-        }
-
-        setSelectedGroupName(groupName);
         setSelectedGroup(group);
       } else {
         // Group not found, reset selected group.
         setSelectedGroup(null);
-        router.push('/groups', undefined, { shallow: true });
+        if (!isLoading) {
+          router.push('/groups', undefined, { shallow: true });
+        }
       }
     }
-  }, [groups.groups, router]);
+  }, [groups.groups, router, isLoading]);
 
   useEffect(() => {
     // Scroll to top when a group is selected
@@ -159,23 +147,20 @@ export default React.memo(function YourGroups({
   }, [selectedGroup]);
 
   const handleSelectGroup = (group: ExtendedGroupType) => {
-    let groupName = 'Untitled Group';
-    try {
-      const metadata = groupSchema.metadataFromJson(group.metadata, false);
-      groupName = metadata?.title ?? 'Untitled Group';
-    } catch (e) {
-      // If JSON parsing fails, fall back to default name
-      console.warn('Failed to parse group metadata:', e);
-    }
-    setSelectedGroupName(groupName);
     setSelectedGroup(group);
-    router.push(`/groups?policyAddress=${group.policies[0]?.address}`, undefined, {
-      shallow: true,
-    });
+    router.push(
+      {
+        pathname: router.pathname,
+        query: { ...router.query, policyAddress: group.policies[0]?.address },
+      },
+      undefined,
+      {
+        shallow: true,
+      }
+    );
   };
 
   const handleBack = () => {
-    setSelectedGroupName('Untitled Group');
     setSelectedGroup(null);
     router.push('/groups', undefined, { shallow: true });
   };
@@ -188,8 +173,7 @@ export default React.memo(function YourGroups({
   const { balances: resolvedBalances, isBalancesLoading: resolvedLoading } =
     useTokenBalancesResolved(address ?? '');
 
-  const { metadatas, isMetadatasLoading, isMetadatasError, refetchMetadatas } =
-    useTokenFactoryDenomsMetadata();
+  const { metadatas, isMetadatasLoading } = useTokenFactoryDenomsMetadata();
   const [currentPageGroupInfo, setCurrentPageGroupInfo] = useState(1);
 
   const {
@@ -356,44 +340,7 @@ export default React.memo(function YourGroups({
                   {isLoading
                     ? Array(isMobile ? 6 : 8)
                         .fill(0)
-                        .map((_, index) => (
-                          <tr key={index} data-testid="skeleton-row">
-                            <td className="bg-secondary rounded-l-[12px] ">
-                              <div className="flex items-center space-x-3">
-                                <div className="skeleton w-11 h-11 rounded-md shrink-0"></div>
-                                <div className="skeleton h-3 w-24"></div>
-                              </div>
-                            </td>
-                            <td className="bg-secondary hidden xl:table-cell">
-                              <div className="skeleton h-2 w-8"></div>
-                            </td>
-                            <td className="bg-secondary hidden sm:table-cell ">
-                              <div className="skeleton h-2 w-16"></div>
-                            </td>
-                            <td className="bg-secondary hidden xl:table-cell">
-                              <div className="skeleton h-2 w-20"></div>
-                            </td>
-                            <td className="bg-secondary hidden lg:table-cell">
-                              <div className="skeleton h-2 w-32"></div>
-                            </td>
-                            <td className="bg-secondary rounded-r-[12px] w-1/6 hidden xs:table-cell">
-                              <div className="flex space-x-2 justify-end">
-                                <button
-                                  className="btn btn-md btn-outline btn-square btn-info"
-                                  disabled
-                                >
-                                  <PiInfo className="w-7 h-7 text-current opacity-50" />
-                                </button>
-                                <button
-                                  className="btn btn-md btn-outline btn-square btn-primary"
-                                  disabled
-                                >
-                                  <MemberIcon className="w-7 h-7 text-current opacity-50" />
-                                </button>
-                              </div>
-                            </td>
-                          </tr>
-                        ))
+                        .map((_, index) => <LoadingGroupRow key={index} />)
                     : paginatedGroupEntries.map((group, index) => (
                         <GroupRow
                           address={address}
@@ -501,13 +448,8 @@ export default React.memo(function YourGroups({
       >
         {selectedGroup && (
           <GroupControls
-            policyAddress={selectedGroup.policies[0]?.address ?? ''}
-            groupName={selectedGroupName}
+            group={selectedGroup}
             onBack={handleBack}
-            policyThreshold={
-              (selectedGroup.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)
-                ?.threshold ?? '0'
-            }
             isLoading={isLoadingGroupInfo}
             currentPage={currentPageGroupInfo}
             setCurrentPage={setCurrentPageGroupInfo}
@@ -525,7 +467,42 @@ export default React.memo(function YourGroups({
       </div>
     </div>
   );
-});
+};
+
+const LoadingGroupRow = () => {
+  return (
+    <tr data-testid="skeleton-row">
+      <td className="bg-secondary rounded-l-[12px] ">
+        <div className="flex items-center space-x-3">
+          <div className="skeleton w-11 h-11 rounded-md shrink-0"></div>
+          <div className="skeleton h-3 w-24"></div>
+        </div>
+      </td>
+      <td className="bg-secondary hidden xl:table-cell">
+        <div className="skeleton h-2 w-8"></div>
+      </td>
+      <td className="bg-secondary hidden sm:table-cell ">
+        <div className="skeleton h-2 w-16"></div>
+      </td>
+      <td className="bg-secondary hidden xl:table-cell">
+        <div className="skeleton h-2 w-20"></div>
+      </td>
+      <td className="bg-secondary hidden lg:table-cell">
+        <div className="skeleton h-2 w-32"></div>
+      </td>
+      <td className="bg-secondary rounded-r-[12px] w-1/6 hidden xs:table-cell">
+        <div className="flex space-x-2 justify-end">
+          <button className="btn btn-md btn-outline btn-square btn-info" disabled>
+            <PiInfo className="w-7 h-7 text-current opacity-50" />
+          </button>
+          <button className="btn btn-md btn-outline btn-square btn-primary" disabled>
+            <MemberIcon className="w-7 h-7 text-current opacity-50" />
+          </button>
+        </div>
+      </td>
+    </tr>
+  );
+};
 
 const GroupRow = React.memo(function GroupRow({
   address,
@@ -589,7 +566,7 @@ const GroupRow = React.memo(function GroupRow({
             <ProfileAvatar walletAddress={policyAddress} />
             <span className="font-medium">{truncateString(groupName, 24)}</span>
           </div>
-          <div className="items-center flex xs:hidden block">
+          <div className="items-center flex xs:hidden">
             <ProfileAvatar walletAddress={policyAddress} />
           </div>
         </td>
@@ -603,10 +580,7 @@ const GroupRow = React.memo(function GroupRow({
           )}
         </td>
         <td className="bg-secondary group-hover:bg-base-300 hidden sm:table-cell w-1/6">
-          {Number(shiftDigits(balance?.amount ?? '0', -6)).toLocaleString(undefined, {
-            maximumFractionDigits: 6,
-          })}{' '}
-          MFX
+          <TokenBalance token={{ amount: balance?.amount, display: 'MFX' }} />
         </td>
         <td className="bg-secondary group-hover:bg-base-300 hidden xl:table-cell w-1/6">
           {`${(group.policies?.[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0'} / ${group.total_weight ?? '0'}`}
diff --git a/components/groups/modals/voteDetailsModal.tsx b/components/groups/modals/voteDetailsModal.tsx
index af1cf71c..6b56f8fb 100644
--- a/components/groups/modals/voteDetailsModal.tsx
+++ b/components/groups/modals/voteDetailsModal.tsx
@@ -3,7 +3,6 @@ import { CheckIcon } from '@heroicons/react/24/outline';
 import { cosmos } from '@liftedinit/manifestjs';
 import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
 import {
-  ProposalSDKType,
   ProposalStatus,
   VoteOption,
   proposalStatusToJSON,
@@ -26,7 +25,6 @@ import VotingPopup from './voteModal';
 
 interface VoteDetailsModalProps {
   policyAddress: string;
-  proposals: ProposalSDKType[];
   proposalId: bigint;
   showVoteModal: boolean;
   onClose: () => void;
diff --git a/components/react/TokenBalance.tsx b/components/react/TokenBalance.tsx
index 5888f208..1b5bc275 100644
--- a/components/react/TokenBalance.tsx
+++ b/components/react/TokenBalance.tsx
@@ -2,24 +2,28 @@ import BigNumber from 'bignumber.js';
 import React from 'react';
 
 import { DenomDisplay } from '@/components';
-import { CombinedBalanceInfo, formatLargeNumber, shiftDigits, truncateString } from '@/utils';
+import { CombinedBalanceInfo, formatLargeNumber, shiftDigits } from '@/utils';
 
+/**
+ * Props for the TokenBalance component.
+ * @param token The token balance information. This should include amount and denomination,
+ *              as well as metadata if available. If amount is missing, it is assumed to be 0.
+ * @param denom The denomination to display. If not provided, the token's display name is used.
+ */
 export interface TokenBalanceProps {
-  token: CombinedBalanceInfo;
+  token: Partial<CombinedBalanceInfo>;
   denom?: string | null;
 }
 
 /**
  * Display the token balance, handling very large amounts of tokens.
- * @param token
- * @param denom
  * @constructor
  */
 export const TokenBalance = ({ token, denom }: TokenBalanceProps) => {
   const units = token.metadata?.denom_units;
   const denomUnit = units?.[units.length - 1];
   const exponent = denomUnit?.exponent ?? 6;
-  denom = (denom ?? denomUnit?.denom ?? token.display).toUpperCase();
+  denom = (denom ?? denomUnit?.denom ?? token.display ?? '<unknown>').toUpperCase();
 
   const [balance, tooltipAmount] = React.useMemo(() => {
     const amount = shiftDigits(token.amount ?? 0, -exponent);
diff --git a/pages/groups/index.tsx b/pages/groups/index.tsx
index 0a095db2..2b4cd7d5 100644
--- a/pages/groups/index.tsx
+++ b/pages/groups/index.tsx
@@ -2,9 +2,7 @@ import { useChain } from '@cosmos-kit/react';
 import Link from 'next/link';
 import React from 'react';
 
-import { GroupsIcon, WalletNotConnected } from '@/components';
-import { SEO } from '@/components';
-import YourGroups from '@/components/groups/components/myGroups';
+import { GroupsIcon, IfWalletConnected, SEO, WalletNotConnected, YourGroups } from '@/components';
 import env from '@/config/env';
 import { useGroupsByMember, useProposalsByPolicyAccountAll } from '@/hooks';
 
@@ -28,28 +26,24 @@ export default function Groups() {
       <SEO title="Groups - Alberto" />
       <div className="grow h-full animate-fadeIn transition-all duration-300">
         <div className="w-full mx-auto">
-          {!isWalletConnected ? (
-            <WalletNotConnected
-              description="Use the button below to connect your wallet and start interacting with your groups."
-              icon={<GroupsIcon className="h-60 w-60 text-primary" />}
-            />
-          ) : isLoading ? (
-            <YourGroups
-              groups={groupByMemberData ?? { groups: [] }}
-              proposals={proposalsByPolicyAccount}
-              isLoading={isLoading}
-            />
-          ) : isError ? (
-            <div className="text-center text-error">Error loading groups or proposals</div>
-          ) : groupByMemberData?.groups.length === 0 ? (
-            <NoGroupsFound />
-          ) : (
-            <YourGroups
-              groups={groupByMemberData ?? { groups: [] }}
-              proposals={proposalsByPolicyAccount}
-              isLoading={isLoading}
-            />
-          )}
+          <IfWalletConnected
+            icon={GroupsIcon}
+            message="Use the button below to connect your wallet and start interacting with your groups."
+          >
+            {isLoading ? (
+              <YourGroups groups={{ groups: [] }} proposals={{}} isLoading={true} />
+            ) : isError ? (
+              <div className="text-center text-error">Error loading groups or proposals</div>
+            ) : groupByMemberData?.groups.length === 0 ? (
+              <NoGroupsFound />
+            ) : (
+              <YourGroups
+                groups={groupByMemberData ?? { groups: [] }}
+                proposals={proposalsByPolicyAccount}
+                isLoading={isLoading}
+              />
+            )}
+          </IfWalletConnected>
         </div>
       </div>
     </div>
diff --git a/tests/mock.ts b/tests/mock.ts
index eb165e79..f0de90a7 100644
--- a/tests/mock.ts
+++ b/tests/mock.ts
@@ -67,11 +67,12 @@ export function mockModule(
 mockModule.force = (name: string, renderMocks: () => Record<string, any>) =>
   mockModule(name, renderMocks, true);
 
-export function mockRouter(): MockedModule {
+export function mockRouter(routerConfig: Record<string, any> = {}): MockedModule {
   return mockModule('next/router', () => ({
     useRouter: jest.fn().mockReturnValue({
       query: {},
       push: jest.fn(),
+      ...routerConfig,
     }),
   }));
 }


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (7)</summary><blockquote>

<details>
<summary>pages/groups/index.tsx (1)</summary><blockquote>

`5-5`: **Remove unused import.**

`WalletNotConnected` is imported but never used. Consider removing it to keep the imports clean and avoid confusion.

```diff
- import { GroupsIcon, IfWalletConnected, SEO, WalletNotConnected, YourGroups } from '@/components';
+ import { GroupsIcon, IfWalletConnected, SEO, YourGroups } from '@/components';
```

</blockquote></details>
<details>
<summary>components/groups/components/myGroups.tsx (2)</summary><blockquote>

`2-2`: **Remove unused import.**

`Transition` doesn’t appear to be utilized in this file. Consider removing it to reduce clutter.

```diff
- import { Transition } from '@headlessui/react';
```

---

`602-602`: **Use dynamic token display if relevant.**

If groups can hold various token types, consider displaying the actual token symbol or name from the metadata rather than hardcoding `'MFX'`.

```diff
- <TokenBalance token={{ amount: balance?.amount, display: 'MFX' }} />
+ <TokenBalance token={{ amount: balance?.amount, display: tokenSymbolFromMetadata }} />
```

</blockquote></details>
<details>
<summary>components/groups/components/Proposals.tsx (1)</summary><blockquote>

`111-245`: **Consider alternate approach to removing `proposalId`.**

Deleting `proposalId` from the query object (line 229) can cause performance overhead in some JS engines. Consider setting it to `undefined` or reassigning a new query object instead.


```diff
- delete query.proposalId;
+ query.proposalId = undefined;
```

**Optional**: Consider using a time utility library (e.g., dayjs/date-fns) for the voting period countdown to reduce custom date/time logic.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 229-229: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.


(lint/performance/noDelete)

</details>

</details>

</blockquote></details>
<details>
<summary>components/groups/components/groupControls.tsx (3)</summary><blockquote>

`42-43`: **Consider using a more robust structure for tab management**

Using a simple array for tab slugs could lead to inconsistencies if tab panels are modified but the slugs aren't updated.


Consider using a more structured approach:

```diff
- const TAB_SLUGS = ['proposals', 'assets', 'activity', 'tokens'];
+ const TABS = [
+   { slug: 'proposals', label: 'Proposals' },
+   { slug: 'assets', label: 'Assets' },
+   { slug: 'activity', label: 'Activity' },
+   { slug: 'tokens', label: 'Tokens' },
+ ];
```

Then you can use this structure throughout your component for consistency.

---

`181-195`: **Consider using the TABS structure for rendering**

If you implement the TABS structure suggested earlier, you could simplify this tab rendering code.


```diff
<Tab.List className="flex flex-row tabs tabs-border tabs-lg">
-  <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
-    Proposals
-  </Tab>
-  <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
-    Assets
-  </Tab>
-  <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
-    Activity
-  </Tab>
-  <Tab className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1">
-    Tokens
-  </Tab>
+  {TABS.map(tab => (
+    <Tab 
+      key={tab.slug}
+      className="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1"
+    >
+      {tab.label}
+    </Tab>
+  ))}
</Tab.List>
```

---

`101-105`: **Potential edge case with tab routing**

When using `setActiveTabInner`, there's a potential race condition where the tab state might be updated but the URL doesn't reflect it.


Consider updating this useEffect to use the `setActiveTab` function for consistency:

```diff
useEffect(() => {
  const { tab } = router.query;
  const maybeIndex = TAB_SLUGS.indexOf(tab as string);
- setActiveTabInner(maybeIndex === -1 ? 0 : maybeIndex);
+ if (maybeIndex !== -1) setActiveTab(maybeIndex);
+ else if (!tab) setActiveTab(0);
}, [router.query, setActiveTab]);
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 814c2b268f31035f947b059c6a936faa93941b2f and 240085846ce762350f2bc60a323a4bb70188c9f7.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `components/factory/forms/ConfirmationForm.tsx` (1 hunks)
* `components/groups/components/Proposals.tsx` (1 hunks)
* `components/groups/components/__tests__/myGroups.test.tsx` (1 hunks)
* `components/groups/components/groupControls.tsx` (8 hunks)
* `components/groups/components/index.tsx` (1 hunks)
* `components/groups/components/myGroups.tsx` (8 hunks)
* `components/groups/modals/voteDetailsModal.tsx` (0 hunks)
* `components/react/TokenBalance.tsx` (2 hunks)
* `pages/groups/index.tsx` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* components/groups/modals/voteDetailsModal.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>components/groups/components/myGroups.tsx (1)</summary>

```
Learnt from: chalabi2
PR: liftedinit/manifest-app#9
File: components/groups/components/myGroups.tsx:184-184
Timestamp: 2025-03-12T20:27:10.521Z
Learning: In `components/groups/components/myGroups.tsx`, it's acceptable to pass the entire `selectedGroup` object as the `policyThreshold` prop to the `GroupProposals` component, even if the expected type is `ThresholdDecisionPolicy`.
```

</details>

</details><details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>components/groups/components/Proposals.tsx</summary>

[error] 229-229: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.


(lint/performance/noDelete)

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Test and Coverage</summary>

<details>
<summary>components/groups/components/groupControls.tsx</summary>

[error] 62-62: TypeError: undefined is not an object (evaluating 'group.policies')

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (20)</summary><blockquote>

<details>
<summary>components/groups/components/index.tsx (1)</summary>

`5-5`: **Clean export addition.**

Adding the Proposals module export aligns with the PR objective of moving proposals logic into a dedicated component, enhancing modularity.

</details>
<details>
<summary>components/factory/forms/ConfirmationForm.tsx (1)</summary>

`203-203`: **Good UX improvement.**

Disabling the "Back" button during signing creates consistent behavior with the "Sign Transaction" button and prevents potential issues from navigating away during an in-progress transaction.

</details>
<details>
<summary>components/groups/components/__tests__/myGroups.test.tsx (1)</summary>

`5-5`: <details>
<summary>❓ Verification inconclusive</summary>

**Import path refactoring.**

The change from direct component import to using the central components index reflects good structural changes in the codebase. Make sure all necessary exports are properly set up in the components index file.


---

🏁 Script executed:

```shell
#!/bin/bash
# Verify the YourGroups component is properly exported from the components index

# Check if YourGroups is exported properly
grep -r "export.*YourGroups" --include="*.tsx" --include="*.ts" components/
```

Length of output: 148

---



</details>

**Attention: Confirm Re-export of YourGroups via the Central Components Index**

The grep output confirms that the YourGroups component is correctly exported in its original file (components/groups/components/myGroups.tsx). However, please double-check that the central components index file (e.g., components/index.ts) also re-exports YourGroups to support the new import style (`import { YourGroups } from '@/components';`). This ensures that the structural refactoring is complete.

</details>
<details>
<summary>components/react/TokenBalance.tsx (2)</summary>

`8-8`: **Good type flexibility improvement.**

Changing from `CombinedBalanceInfo` to `Partial<CombinedBalanceInfo>` makes the component more resilient to incomplete data situations, which is particularly helpful during loading states or when partial data is available.

---

`22-22`: **Enhanced fallback handling.**

Adding `'<unknown>'` as a final fallback ensures the component always displays something meaningful rather than undefined, improving error resilience.

</details>
<details>
<summary>pages/groups/index.tsx (1)</summary>

`29-46`: **Solid refactor with `IfWalletConnected`.**

This approach cleanly separates wallet connection logic from the groups rendering according to different states (loading, error, no groups, etc.). It promotes clarity and maintainability.

</details>
<details>
<summary>components/groups/components/myGroups.tsx (6)</summary>

`13-13`: **LGTM!**

Including `TokenBalance` here is appropriate and aligns with the usage at line 602.

---

`52-60`: **Good transition to a named export.**

Exporting `YourGroups` as a named arrow function can help with consistency and easier refactoring across the codebase.

---

`149-151`: **Automatic redirection on missing group.**

The logic to redirect the user back to `/groups` if no matching group is found (once loading is complete) seems correct.

---

`154-154`: **Dependency array updated.**

Adding `isLoading` to the dependency array ensures the redirect logic will only trigger post-loading, reducing the risk of premature navigation.

---

`360-361`: **Clear skeleton design.**

Using a mapped array for skeleton rows and rendering your new `LoadingGroupRow` streamlines the loading indicator.

---

`469-469`: **Named prop usage.**

Replacing `policyAddress` with `group` follows the new, more descriptive structure for `GroupControls`.

</details>
<details>
<summary>components/groups/components/Proposals.tsx (4)</summary>

`1-16`: **Imports look good.**

These initial imports correctly pull in the group and proposal types, as well as Next.js and React utilities.

---

`48-55`: **Readable type transformation.**

`getHumanReadableType` is a neat helper for converting message types to a friendlier format.

---

`57-110`: **Logical breakdown with `GroupProposals`.**

The component’s structure for enumerating proposals in a table is cleanly handled, and the separate `ProposalRow` helps keep things modular.

---

`18-46`: <details>
<summary>✅ Verification successful</summary>

**Verify integer-based thresholds.**

`isProposalPassing` uses `BigInt` for vote counts and threshold checks. Ensure the decision policy always provides integer thresholds to avoid potential issues with fractional or decimal-based thresholds.

---

🏁 Script executed:

```shell
#!/bin/bash
# This script greps for any mention of threshold in the code
# to confirm integer usage for thresholds.
rg -A 5 "threshold"
```

Length of output: 17407

---



</details>

**Action: Confirm integer conversion for decision policy thresholds in proposals.**  

After reviewing the code and grep results, it’s clear that in the proposal logic (specifically in **components/groups/components/Proposals.tsx**) the vote counts and the threshold are converted to BigInt—ensuring that arithmetic relies solely on integer values. The supporting files (e.g., **UpdateGroupModal.tsx**, test files, and form validations) convert any incoming threshold value (even when provided as a string) to a number before it’s later converted to BigInt.  

- The conversion using `Number(maybeThreshold || 1)` in the modal and subsequent use of `BigInt(policyThreshold)` in the proposal component confirms that integer values are enforced.  
- Note that while a fractional threshold appears in **pages/index.tsx**, it’s used for a different purpose (e.g., Intersection Observer configuration) and is not related to decision policy logic.  

In summary, the current implementation correctly ensures that decision policy thresholds are handled as integers. Please double-check that any external APIs or properties supplying thresholds for proposals continue to conform to this integer constraint.

</details>
<details>
<summary>components/groups/components/groupControls.tsx (4)</summary>

`83-99`: **Good refactoring using useCallback**

The implementation of `setActiveTab` using `useCallback` is a good practice, ensuring the function doesn't get recreated on every render while maintaining proper dependencies.

---

`197-283`: **Great refactoring with GroupProposals component**

Moving the proposal display logic to a dedicated `GroupProposals` component is a significant improvement. This follows the Single Responsibility Principle and makes the codebase more maintainable.

---

`212-218`: **Excellent move to dedicated component**

Moving the proposals display logic to a dedicated `GroupProposals` component greatly simplifies this file and improves separation of concerns.

---

`311-320`: <details>
<summary>❓ Verification inconclusive</summary>

**Check negative margin on the Tokens tab**

You've applied a `-mt-7` class to the Tokens tab panel, which creates a negative top margin. This could potentially cause layout issues.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if this negative margin is necessary or if it could cause layout issues
grep -r "mt-7" --include="*.tsx" --include="*.jsx" .
grep -r "DenomList" --include="*.tsx" --include="*.jsx" .
```

Length of output: 1031

---



</details>

**Revisit the Negative Margin on the Tokens Tab**

In `components/groups/components/groupControls.tsx` (around lines 311–320), the `<Tab.Panel>` uses a `-mt-7` class that isn't common elsewhere in the repository. Please verify whether this negative top margin is intentionally used to adjust the layout of the Tokens tab or if it might lead to unintended layout issues. If this offset is necessary as a workaround, consider adding an inline comment explaining its purpose to aid future maintenance.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T18:27:32Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 1**

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>components/groups/components/groupControls.tsx (2)</summary><blockquote>

`61-61`: **Optional safety check for `group` object.**

If there's any possibility that `group` might be `undefined`, consider using optional chaining on `group` itself (e.g., `group?.policies?.[0]?.address ?? ''`). Otherwise, no concerns here.

---

`315-326`: **Tokens tab panel logic.**

Integrating `DenomList` here is consistent with the tab design. If tokens can be numerous, consider pagination in the future.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 240085846ce762350f2bc60a323a4bb70188c9f7 and 6c2778183dc89e492af46ea2671e91383a49db1f.

</details>

<details>
<summary>📒 Files selected for processing (10)</summary>

* `components/factory/forms/ConfirmationForm.tsx` (1 hunks)
* `components/groups/components/Proposals.tsx` (1 hunks)
* `components/groups/components/__tests__/groupProposals.test.tsx` (1 hunks)
* `components/groups/components/__tests__/myGroups.test.tsx` (1 hunks)
* `components/groups/components/groupControls.tsx` (8 hunks)
* `components/groups/components/index.tsx` (1 hunks)
* `components/groups/components/myGroups.tsx` (7 hunks)
* `components/groups/modals/voteDetailsModal.tsx` (0 hunks)
* `components/react/TokenBalance.tsx` (2 hunks)
* `pages/groups/index.tsx` (2 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* components/groups/modals/voteDetailsModal.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (5)</summary>

* components/groups/components/index.tsx
* components/groups/components/__tests__/myGroups.test.tsx
* components/factory/forms/ConfirmationForm.tsx
* pages/groups/index.tsx
* components/react/TokenBalance.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>components/groups/components/groupControls.tsx</summary>

[warning] 83-94: components/groups/components/groupControls.tsx#L83-L94
Added lines #L83 - L94 were not covered by tests

---

[warning] 239-252: components/groups/components/groupControls.tsx#L239-L252
Added lines #L239 - L252 were not covered by tests

---

[warning] 258-271: components/groups/components/groupControls.tsx#L258-L271
Added lines #L258 - L271 were not covered by tests

---

[warning] 276-276: components/groups/components/groupControls.tsx#L276
Added line #L276 was not covered by tests

</details>
<details>
<summary>components/groups/components/Proposals.tsx</summary>

[warning] 18-44: components/groups/components/Proposals.tsx#L18-L44
Added lines #L18 - L44 were not covered by tests

---

[warning] 126-126: components/groups/components/Proposals.tsx#L126
Added line #L126 was not covered by tests

---

[warning] 147-148: components/groups/components/Proposals.tsx#L147-L148
Added lines #L147 - L148 were not covered by tests

---

[warning] 152-155: components/groups/components/Proposals.tsx#L152-L155
Added lines #L152 - L155 were not covered by tests

---

[warning] 164-165: components/groups/components/Proposals.tsx#L164-L165
Added lines #L164 - L165 were not covered by tests

---

[warning] 168-169: components/groups/components/Proposals.tsx#L168-L169
Added lines #L168 - L169 were not covered by tests

---

[warning] 171-179: components/groups/components/Proposals.tsx#L171-L179
Added lines #L171 - L179 were not covered by tests

---

[warning] 181-181: components/groups/components/Proposals.tsx#L181
Added line #L181 was not covered by tests

---

[warning] 188-196: components/groups/components/Proposals.tsx#L188-L196
Added lines #L188 - L196 were not covered by tests

---

[warning] 226-236: components/groups/components/Proposals.tsx#L226-L236
Added lines #L226 - L236 were not covered by tests

</details>
<details>
<summary>components/groups/components/myGroups.tsx</summary>

[warning] 136-138: components/groups/components/myGroups.tsx#L136-L138
Added lines #L136 - L138 were not covered by tests

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>components/groups/components/Proposals.tsx</summary>

[error] 229-229: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.


(lint/performance/noDelete)

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (21)</summary><blockquote>

<details>
<summary>components/groups/components/__tests__/groupProposals.test.tsx (1)</summary>

`11-20`: **New test props structure looks good.**

Switching from a single `policyAddress` to a `group` object is logical and aligns with the multiple policies structure. No issues noted.

</details>
<details>
<summary>components/groups/components/groupControls.tsx (8)</summary>

`2-2`: **New imports and constants are well-structured.**

The inclusion of `Tab` from `@headlessui/react` and the declaration of `TAB_SLUGS` are consistent with the new tab-based UI logic. No issues found.  



Also applies to: 9-9, 14-14, 18-18, 20-20, 43-43, 45-45

---

`67-67`: **State declaration for tab indexing is clear.**

Using a separate stateful variable `activeTab` is straightforward and improves readability. No issues found.

---

`82-98`: **Add test coverage for tab routing logic.**

This callback updates the URL query with a shallow push. Consider adding tests to ensure the correct query param is set, addressing the codecov/patch warning for lines #L83-L94.  

Do you want assistance generating tests for this callback?

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 83-94: components/groups/components/groupControls.tsx#L83-L94
Added lines #L83 - L94 were not covered by tests

</details>

</details>

---

`100-103`: **Effect to sync `activeTab` with query param is sound.**

Your approach of defaulting to index 0 if the URL param is unknown is good.

---

`107-107`: **Memoized group name.**

The usage of `metadataFromJson` is consistent with your schema approach. Good job handling defaults with `"Untitled Group"`.

---

`113-113`: **Filtering proposals to exclude rejected/withdrawn.**

Looks correct. Ensure this aligns with your product requirements if more statuses need to be filtered out.

---

`185-287`: **Proposal tab panel logic and pagination.**

The pagination approach (lines #L216-286) appears solid. However, codecov/patch flags these lines as untested. Consider covering edge cases such as:  
• Zero proposals  
• Next/previous button disabled states  
• Large page jumps

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 239-252: components/groups/components/groupControls.tsx#L239-L252
Added lines #L239 - L252 were not covered by tests

---

[warning] 258-271: components/groups/components/groupControls.tsx#L258-L271
Added lines #L258 - L271 were not covered by tests

---

[warning] 276-276: components/groups/components/groupControls.tsx#L276
Added line #L276 was not covered by tests

</details>

</details>

---

`289-289`: **Assets tab panel.**

Clean separation of logic into each `<Tab.Panel>`. The usage is sound.

</details>
<details>
<summary>components/groups/components/Proposals.tsx (5)</summary>

`1-16`: **New imports are well-organized.**

All imported entities are relevant to proposals logic. No concerns here.

---

`18-46`: **Check boundary conditions for threshold logic.**

`isProposalPassing` correctly handles `yes`, `no`, `veto`, and `abstain`. Ensure `policyThreshold` can’t be negative or abnormally large. Otherwise, looks fine.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 18-44: components/groups/components/Proposals.tsx#L18-L44
Added lines #L18 - L44 were not covered by tests

</details>

</details>

---

`48-56`: **Utility function for message type is concise.**

Simple approach to convert message type into a more readable string. No issues found.

---

`57-109`: **`GroupProposals` component covers multiple proposals.**

Overall structure to map proposals and pass them into `ProposalRow` is good. The codecov/patch warnings (lines #L18-L44 and others) hint at missing tests for the tallies or the table. Consider adding coverage for how tallies are fetched.

---

`111-228`: **`ProposalRow` structure effectively manages show/hide for vote modal.**

The logic for time remaining is correct and easily readable. Good job hooking into the router query to open the modal automatically.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 126-126: components/groups/components/Proposals.tsx#L126
Added line #L126 was not covered by tests

---

[warning] 147-148: components/groups/components/Proposals.tsx#L147-L148
Added lines #L147 - L148 were not covered by tests

---

[warning] 152-155: components/groups/components/Proposals.tsx#L152-L155
Added lines #L152 - L155 were not covered by tests

---

[warning] 164-165: components/groups/components/Proposals.tsx#L164-L165
Added lines #L164 - L165 were not covered by tests

---

[warning] 168-169: components/groups/components/Proposals.tsx#L168-L169
Added lines #L168 - L169 were not covered by tests

---

[warning] 171-179: components/groups/components/Proposals.tsx#L171-L179
Added lines #L171 - L179 were not covered by tests

---

[warning] 181-181: components/groups/components/Proposals.tsx#L181
Added line #L181 was not covered by tests

---

[warning] 188-196: components/groups/components/Proposals.tsx#L188-L196
Added lines #L188 - L196 were not covered by tests

</details>

</details>

</details>
<details>
<summary>components/groups/components/myGroups.tsx (7)</summary>

`12-12`: **Importing `TokenBalance` safely.**

This appears correct and consistent with the usage. No issues found.

---

`50-50`: **Converted to a named export.**

Using a named export for `YourGroups` can improve clarity and reduce default import confusion.

---

`58-58`: **Props destructuring.**

Clean destructuring approach. No issues found.

---

`136-138`: **Conditional redirect if group is unavailable.**

Returning the user to `/groups` once loading completes is user-friendly. If there's a chance the group arrives late, confirm no race conditions.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 136-138: components/groups/components/myGroups.tsx#L136-L138
Added lines #L136 - L138 were not covered by tests

</details>

</details>

---

`438-465`: **`GroupControls` integration.**

Switching from `policyAddress` to `group` is consistent with the overall refactor. Code looks good.

---

`467-499`: **`LoadingGroupRow` provides a clear skeleton layout.**

The approach for displaying skeleton rows is consistent and addresses the earlier React key concerns. No issues here.

---

`578-578`: **Using `<TokenBalance>` for group balance.**

Replacing manual formatting with `<TokenBalance>` centralizes logic. This is a good refactor.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T18:50:40Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (3)</summary><blockquote>

<details>
<summary>components/groups/components/Proposals.tsx (3)</summary><blockquote>

`131-132`: **Add type guard for decision_policy.**

The code assumes that the decision policy is always `ThresholdDecisionPolicySDKType`, but there might be other types of decision policies in the future.


```diff
- const policyThreshold =
-   (group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+ const decisionPolicy = group.policies[0]?.decision_policy;
+ const policyThreshold = 
+   (decisionPolicy && 'threshold' in decisionPolicy) 
+     ? (decisionPolicy as ThresholdDecisionPolicySDKType).threshold 
+     : '0';
```

---

`214-215`: **Improve type safety when accessing proposal message type.**

Using `as any` bypasses TypeScript's type checking, which could lead to runtime errors if the structure changes.


```diff
- {proposal.messages.length > 0
-   ? getHumanReadableType((proposal.messages[0] as any)['@type'])
-   : 'No messages'}
+ {proposal.messages.length > 0
+   ? getHumanReadableType(proposal.messages[0]?.['@type'] ?? 'Unknown')
+   : 'No messages'}
```

---

`205-206`: **Consider simplifying complex conditional class names.**

The class name string with multiple conditional expressions is hard to read and maintain. Consider using a utility function or library like `clsx` or `tailwind-merge` to handle conditional class names.


```diff
- className={`bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:rounded-none xxs:rounded-r-[12px] xs:rounded-r-[12px] xl:rounded-r-none`}
+ // Using a utility function (define this somewhere in your utils)
+ className={classNames(
+   "bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%]",
+   {"rounded-r-[12px]": isSmallScreen, "rounded-none": !isSmallScreen || isXLScreen}
+ )}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 6c2778183dc89e492af46ea2671e91383a49db1f and 52d942b9538c68bc61d38bec4f3af7fcd8f6ba39.

</details>

<details>
<summary>📒 Files selected for processing (2)</summary>

* `components/groups/components/Proposals.tsx` (1 hunks)
* `components/groups/components/groupControls.tsx` (8 hunks)

</details>

<details>
<summary>🔇 Additional comments (9)</summary><blockquote>

<details>
<summary>components/groups/components/groupControls.tsx (6)</summary>

`61-61`: **Good fix for handling potential undefined policies.**

The optional chaining is correctly implemented to handle cases where `group` or `policies` might be undefined, addressing the issue mentioned in the past review comment.

---

`67-98`: **Well-structured tab management with improved URL handling.**

The refactoring from string-based to index-based tab management is well done. The `useCallback` for `setActiveTab` is appropriate to avoid recreating the function on each render, and the URL update logic with shallow routing maintains a good user experience.

---

`106-109`: **Good use of useMemo for derived group name.**

Using `useMemo` for extracting the group name from metadata is appropriate to avoid unnecessary recalculations on renders that don't involve metadata changes.

---

`185-199`: **Great improvement with HeadlessUI Tab components.**

Replacing the custom tab implementation with HeadlessUI Tab components improves accessibility and reduces boilerplate code while maintaining the same visual appearance.

---

`216-223`: **Good encapsulation of proposal display logic.**

Moving the proposal display logic to a dedicated `GroupProposals` component improves code organization and maintainability.

---

`229-273`: **Well-implemented pagination with ellipsis for large page counts.**

The pagination implementation handles the complexity of showing relevant page numbers with ellipsis for large page counts, improving user experience.

</details>
<details>
<summary>components/groups/components/Proposals.tsx (3)</summary>

`18-46`: **Well-designed vote counting logic.**

The `isProposalPassing` function correctly handles the various voting outcomes including ties and threshold requirements. The function returns all necessary information for determining proposal status.

---

`48-55`: **Nice utility for human-readable message types.**

The `getHumanReadableType` function effectively converts technical message type strings to user-friendly text by removing namespaces and adding spaces between capitalized words.

---

`228-230`: **Good implementation avoiding the `delete` operator.**

You've correctly implemented object destructuring to avoid using the `delete` operator on router.query, addressing the performance concern mentioned in the past review comment.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T00:39:21Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (5)</summary><blockquote>

<details>
<summary>components/groups/components/groupControls.tsx (1)</summary><blockquote>

`106-109`: **Consider adding a null check for group**

While you're using optional chaining with `metadata?.title`, there's no check for `group` itself before accessing `group.metadata`.

```diff
const groupName = useMemo(() => {
-  const metadata = groupSchema.metadataFromJson(group.metadata, false);
+  const metadata = groupSchema.metadataFromJson(group?.metadata, false);
  return metadata?.title ?? 'Untitled Group';
}, [group?.metadata]);
```

</blockquote></details>
<details>
<summary>components/groups/components/Proposals.tsx (4)</summary><blockquote>

`48-55`: **Consider handling edge cases in getHumanReadableType**

The function should handle empty strings and invalid formats to prevent potential issues.

```diff
function getHumanReadableType(type: string): string {
+  if (!type) return 'Unknown';
  const parts = type.split('.');
  const lastPart = parts[parts.length - 1];
  return lastPart
    .replace('Msg', '')
    .replace(/([A-Z])/g, ' $1')
    .trim();
}
```

---

`134-157`: **Validate voting_period_end before creating Date object**

The current implementation doesn't validate that `proposal?.voting_period_end` is a valid date string before creating a Date object, which could result in an invalid date.

```diff
-const endTime = new Date(proposal?.voting_period_end);
+const endTime = proposal?.voting_period_end ? new Date(proposal.voting_period_end) : new Date();
+const isEndTimeValid = !isNaN(endTime.getTime());
+
const now = new Date();
const msPerMinute = 1000 * 60;
const msPerHour = msPerMinute * 60;
const msPerDay = msPerHour * 24;

-const diff = endTime.getTime() - now.getTime();
+const diff = isEndTimeValid ? endTime.getTime() - now.getTime() : 0;

let timeLeft: string;

-if (diff <= 0) {
+if (!isEndTimeValid || diff <= 0) {
  timeLeft = 'none';
} else if (diff >= msPerDay) {
  // Rest of the time calculation logic...
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 147-148: components/groups/components/Proposals.tsx#L147-L148
Added lines #L147 - L148 were not covered by tests

---

[warning] 152-155: components/groups/components/Proposals.tsx#L152-L155
Added lines #L152 - L155 were not covered by tests

</details>

</details>

---

`214-214`: **Replace `any` type with a more specific type**

Using `any` bypasses TypeScript's type checking, which can lead to runtime errors.

```diff
-? getHumanReadableType((proposal.messages[0] as any)['@type'])
+? getHumanReadableType((proposal.messages[0] as { '@type': string })['@type'])
```

---

`62-109`: **Consider memoizing filtered tallies**

The current implementation finds the matching tally for each proposal in the render method, which could be inefficient for large lists.

Consider memoizing the filtered tallies to improve performance:

```diff
export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));

+  const proposalTallies = useMemo(() => {
+    return proposals.map(proposal => ({
+      proposal,
+      tally: tallies.find(t => t.proposalId === proposal.id)?.tally
+    }));
+  }, [proposals, tallies]);

  return (
    <>
      <table
        className="table w-full border-separate border-spacing-y-3 -mt-6"
        aria-label="Group proposals"
      >
        <thead>...</thead>
        <tbody className="space-y-4">
-          {proposals.map(proposal => (
+          {proposalTallies.map(({ proposal, tally }) => (
            <ProposalRow
              key={`proposal-${proposal.id}`}
              group={group}
              proposal={proposal}
-              tally={tallies.find(t => t.proposalId === proposal.id)?.tally}
+              tally={tally}
            />
          ))}
        </tbody>
      </table>
    </>
  );
};
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 52d942b9538c68bc61d38bec4f3af7fcd8f6ba39 and f3d5e5b5a052d6b76f803f36f7f196e657e9756d.

</details>

<details>
<summary>📒 Files selected for processing (3)</summary>

* `components/groups/components/Proposals.tsx` (1 hunks)
* `components/groups/components/groupControls.tsx` (8 hunks)
* `components/react/TokenBalance.tsx` (1 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* components/react/TokenBalance.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>components/groups/components/Proposals.tsx</summary>

[warning] 18-44: components/groups/components/Proposals.tsx#L18-L44
Added lines #L18 - L44 were not covered by tests

---

[warning] 126-126: components/groups/components/Proposals.tsx#L126
Added line #L126 was not covered by tests

---

[warning] 147-148: components/groups/components/Proposals.tsx#L147-L148
Added lines #L147 - L148 were not covered by tests

---

[warning] 152-155: components/groups/components/Proposals.tsx#L152-L155
Added lines #L152 - L155 were not covered by tests

---

[warning] 164-165: components/groups/components/Proposals.tsx#L164-L165
Added lines #L164 - L165 were not covered by tests

---

[warning] 168-169: components/groups/components/Proposals.tsx#L168-L169
Added lines #L168 - L169 were not covered by tests

---

[warning] 171-179: components/groups/components/Proposals.tsx#L171-L179
Added lines #L171 - L179 were not covered by tests

---

[warning] 181-181: components/groups/components/Proposals.tsx#L181
Added line #L181 was not covered by tests

---

[warning] 188-196: components/groups/components/Proposals.tsx#L188-L196
Added lines #L188 - L196 were not covered by tests

---

[warning] 226-228: components/groups/components/Proposals.tsx#L226-L228
Added lines #L226 - L228 were not covered by tests

</details>
<details>
<summary>components/groups/components/groupControls.tsx</summary>

[warning] 83-94: components/groups/components/groupControls.tsx#L83-L94
Added lines #L83 - L94 were not covered by tests

---

[warning] 239-252: components/groups/components/groupControls.tsx#L239-L252
Added lines #L239 - L252 were not covered by tests

---

[warning] 258-271: components/groups/components/groupControls.tsx#L258-L271
Added lines #L258 - L271 were not covered by tests

---

[warning] 276-276: components/groups/components/groupControls.tsx#L276
Added line #L276 was not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>components/groups/components/groupControls.tsx (4)</summary>

`61-61`: **Great use of optional chaining!**

This properly addresses the previous issue where `group.policies` could be undefined, as highlighted in the past review comments. The optional chaining ensures safer access to nested properties.

---

`185-326`: **Clean integration of Headless UI Tabs**

The refactoring from custom tab management to `@headlessui/react` Tab components results in cleaner, more maintainable code. This aligns well with the PR objective of simplifying tab management.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 239-252: components/groups/components/groupControls.tsx#L239-L252
Added lines #L239 - L252 were not covered by tests

---

[warning] 258-271: components/groups/components/groupControls.tsx#L258-L271
Added lines #L258 - L271 were not covered by tests

---

[warning] 276-276: components/groups/components/groupControls.tsx#L276
Added line #L276 was not covered by tests

</details>

</details>

---

`216-222`: **Good implementation of the new GroupProposals component**

Extracting the proposal display logic to a dedicated component improves code organization, which aligns with the PR objective of moving proposals logic into a dedicated component.

---

`82-98`: <details>
<summary>❓ Verification inconclusive</summary>

**Good refactoring with useCallback for tab management**

Using `useCallback` for the `setActiveTab` function is a performance improvement, as it prevents unnecessary re-renders by memoizing the function.


The static analysis indicates this code isn't covered by tests. Consider adding tests for this tab management logic:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for any existing tests for tab navigation in GroupControls
rg -A 2 -B 2 "setActiveTab|activeTab" components/groups/components/__tests__
```

Length of output: 78

---



</details>

**Solid refactoring with room for test coverage improvement**

The use of `useCallback` for `setActiveTab` is well done, effectively preventing unnecessary renders and ensuring optimal routing updates. However, our script to locate tests for the tab navigation logic produced no output in the expected test directory. Please verify manually whether tests exist elsewhere or consider adding tests specifically to cover this functionality.

- **Location:** `components/groups/components/groupControls.tsx`, Lines 82-98

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 83-94: components/groups/components/groupControls.tsx#L83-L94
Added lines #L83 - L94 were not covered by tests

</details>

</details>

</details>
<details>
<summary>components/groups/components/Proposals.tsx (1)</summary>

`228-229`: **Excellent use of object destructuring**

Using object destructuring to exclude `proposalId` from the query is better than using the `delete` operator as previously suggested in a review comment. This approach is more performant and avoids mutating the original object.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T00:45:30Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>🧹 Nitpick comments (6)</summary><blockquote>

<details>
<summary>components/groups/components/myGroups.tsx (2)</summary><blockquote>

`275-275`: **Reminder to address the TODO comment.**

There's a TODO comment about moving code to a `CombinedBalanceInfo` factory function that should be addressed in future refactoring.

Would you like me to provide an implementation of this factory function to help with the refactoring?

---

`1-659`: **General feedback on component complexity.**

While the current changes are good improvements, the YourGroups component is still quite large and handles many responsibilities (filtering, pagination, group selection, balance calculations, etc.). Consider further decomposing this component in future refactoring efforts.

Some suggestions:
- Extract pagination logic into a reusable component
- Move token balance calculation logic to a custom hook
- Extract the filtering and search functionality to a separate component

This would improve maintainability, testability, and readability of the code.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 522-522: Change to an optional chain.

Unsafe fix: Change to an optional chain.


(lint/complexity/useOptionalChain)

</details>
<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 136-138: components/groups/components/myGroups.tsx#L136-L138
Added lines #L136 - L138 were not covered by tests

</details>

</details>

</blockquote></details>
<details>
<summary>components/groups/components/Proposals.tsx (2)</summary><blockquote>

`135-158`: **Consider extracting time calculation logic to a reusable utility function**

The time remaining calculation logic is complex and could be moved to a separate utility function for better reusability and testability. This would also help improve test coverage for this logic.

```diff
- const endTime = new Date(proposal?.voting_period_end);
- const now = new Date();
- const msPerMinute = 1000 * 60;
- const msPerHour = msPerMinute * 60;
- const msPerDay = msPerHour * 24;
-
- const diff = endTime.getTime() - now.getTime();
-
- let timeLeft: string;
-
- if (diff <= 0) {
-   timeLeft = 'none';
- } else if (diff >= msPerDay) {
-   const days = Math.floor(diff / msPerDay);
-   timeLeft = `${days} day${days === 1 ? '' : 's'}`;
- } else if (diff >= msPerHour) {
-   const hours = Math.floor(diff / msPerHour);
-   timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
- } else if (diff >= msPerMinute) {
-   const minutes = Math.floor(diff / msPerMinute);
-   timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
- } else {
-   timeLeft = 'less than a minute';
- }
+ const timeLeft = getTimeRemaining(proposal?.voting_period_end);
```

Where you would implement a utility function like:

```typescript
export function getTimeRemaining(endTimeISO?: string): string {
  if (!endTimeISO) return 'none';
  
  const endTime = new Date(endTimeISO);
  const now = new Date();
  const msPerMinute = 1000 * 60;
  const msPerHour = msPerMinute * 60;
  const msPerDay = msPerHour * 24;

  const diff = endTime.getTime() - now.getTime();

  if (diff <= 0) {
    return 'none';
  } else if (diff >= msPerDay) {
    const days = Math.floor(diff / msPerDay);
    return `${days} day${days === 1 ? '' : 's'}`;
  } else if (diff >= msPerHour) {
    const hours = Math.floor(diff / msPerHour);
    return `${hours} hour${hours === 1 ? '' : 's'}`;
  } else if (diff >= msPerMinute) {
    const minutes = Math.floor(diff / msPerMinute);
    return `${minutes} minute${minutes === 1 ? '' : 's'}`;
  } else {
    return 'less than a minute';
  }
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 148-149: components/groups/components/Proposals.tsx#L148-L149
Added lines #L148 - L149 were not covered by tests

---

[warning] 153-156: components/groups/components/Proposals.tsx#L153-L156
Added lines #L153 - L156 were not covered by tests

</details>

</details>

---

`218-219`: **Avoid type casting to `any`**

Type casting to `any` bypasses TypeScript's type checking. Consider creating a proper type for the message structure or use a more specific type than `any`.

```diff
- ? getHumanReadableType((proposal.messages[0] as any)['@type'])
+ ? getHumanReadableType((proposal.messages[0] as { '@type': string })['@type'])
```

</blockquote></details>
<details>
<summary>components/groups/components/__tests__/groupProposals.test.tsx (2)</summary><blockquote>

`17-17`: **Consider adding proper typing instead of using `as any`**

Using `as any` bypasses TypeScript's type checking. Consider creating or using the actual type for the policy object instead of using type assertion.

```diff
- } as any,
+ } as Policy, // Use the actual Policy type
```

---

`19-19`: **Consider using proper typing instead of `as any`**

Similarly, using `as any` for the group bypasses TypeScript's type checking. Consider using the actual `ExtendedGroupType` from your imports.

```diff
- } as any,
+ } as ExtendedGroupType,
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between f3d5e5b5a052d6b76f803f36f7f196e657e9756d and 29d4974d5a1a967c497ee9b5935921777017f24d.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `components/groups/components/Proposals.tsx` (1 hunks)
* `components/groups/components/__tests__/Proposals.test.tsx` (1 hunks)
* `components/groups/components/__tests__/groupProposals.test.tsx` (2 hunks)
* `components/groups/components/__tests__/myGroups.test.tsx` (4 hunks)
* `components/groups/components/myGroups.tsx` (10 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* components/groups/components/__tests__/myGroups.test.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>components/groups/components/Proposals.tsx (1)</summary>

```
Learnt from: chalabi2
PR: liftedinit/manifest-app#9
File: components/groups/components/groupProposals.tsx:207-208
Timestamp: 2025-03-12T20:27:10.521Z
Learning: In the `GroupProposals` component, redundant checks for `group.policies.length > 0` are unnecessary because prior validations ensure that `group.policies[0]` is safe to access.
```

</details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>components/groups/components/Proposals.tsx</summary>

[warning] 127-127: components/groups/components/Proposals.tsx#L127
Added line #L127 was not covered by tests

---

[warning] 148-149: components/groups/components/Proposals.tsx#L148-L149
Added lines #L148 - L149 were not covered by tests

---

[warning] 153-156: components/groups/components/Proposals.tsx#L153-L156
Added lines #L153 - L156 were not covered by tests

---

[warning] 165-166: components/groups/components/Proposals.tsx#L165-L166
Added lines #L165 - L166 were not covered by tests

---

[warning] 169-170: components/groups/components/Proposals.tsx#L169-L170
Added lines #L169 - L170 were not covered by tests

---

[warning] 172-180: components/groups/components/Proposals.tsx#L172-L180
Added lines #L172 - L180 were not covered by tests

---

[warning] 182-182: components/groups/components/Proposals.tsx#L182
Added line #L182 was not covered by tests

---

[warning] 189-200: components/groups/components/Proposals.tsx#L189-L200
Added lines #L189 - L200 were not covered by tests

---

[warning] 230-232: components/groups/components/Proposals.tsx#L230-L232
Added lines #L230 - L232 were not covered by tests

</details>
<details>
<summary>components/groups/components/myGroups.tsx</summary>

[warning] 136-138: components/groups/components/myGroups.tsx#L136-L138
Added lines #L136 - L138 were not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (11)</summary><blockquote>

<details>
<summary>components/groups/components/myGroups.tsx (5)</summary>

`50-51`: **LGTM! Component signature change improves consistency.**

The change from a default export with React.memo to a named export as an arrow function improves consistency with modern React patterns. This is a positive architectural change that will make the codebase more maintainable.



Also applies to: 58-58, 471-471

---

`344-344`: **LGTM! LoadingGroupRow extraction.**

Extracting the loading skeleton into a separate component improves code organization and maintainability.

---

`473-506`: **LGTM! Clean implementation of LoadingGroupRow component.**

The extracted component is well-structured and follows the same visual pattern as the actual row. Good use of semantic HTML and test identifiers.

---

`584-584`: **LGTM! TokenBalance component usage simplifies code.**

Using the TokenBalance component instead of manual formatting improves consistency and maintainability across the application.

---

`452-452`: **LGTM! Props refactoring improves component flexibility.**

Passing the entire `group` object instead of just the `policyAddress` gives the component more flexibility and access to all group properties. This is a good practice for component composition.

</details>
<details>
<summary>components/groups/components/__tests__/Proposals.test.tsx (1)</summary>

`1-99`: **Excellent test coverage for the isProposalPassing function!**

This test file covers a comprehensive set of scenarios for the `isProposalPassing` function, addressing the previously identified test coverage gap. The tests verify:
- Passing votes
- Empty tally cases
- Threshold calculations
- Various vote outcomes (YES, NO, tie)

Each test is clearly structured with proper test data and assertions. Great job implementing focused unit tests that validate all branches of the function.

</details>
<details>
<summary>components/groups/components/Proposals.tsx (4)</summary>

`19-47`: **Well-implemented vote calculation with thorough test coverage**

The `isProposalPassing` function properly uses BigInt for vote calculations and handles all necessary conditions: threshold checking, tie detection, and vote distribution analysis. The implementation is clean, readable, and now covered by comprehensive tests.

---

`49-56`: **Clean utility function for type formatting**

The `getHumanReadableType` function effectively transforms type strings into human-readable format through simple yet effective string manipulation.

---

`131-133`: **Correctly implemented optional chaining**

Good implementation of optional chaining for accessing nested properties, which addresses a previous review comment about potential undefined access issues.

---

`232-233`: **Well-implemented object destructuring for query manipulation**

Great job using object destructuring to handle the router query update. This is a clean approach that avoids the `delete` operator which had been flagged in a previous review.

</details>
<details>
<summary>components/groups/components/__tests__/groupProposals.test.tsx (1)</summary>

`10-33`: **Props structure correctly updated to align with component changes**

The `mockProps` object has been appropriately updated to reflect the component's new structure, replacing the `policyAddress` property with a `group` object containing policies. The additional props support the enhanced functionality of the refactored component.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T04:13:58Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>components/groups/components/Proposals.tsx (1)</summary><blockquote>

`145-147`: **Consider extracting policy threshold retrieval to a utility function.**

The policy threshold retrieval logic is duplicated across components. Consider extracting this to a reusable utility function to promote code reuse and maintainability.


```diff
- const policyAddress = group?.policies[0]?.address ?? '';
- const policyThreshold =
-   (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+ const policyAddress = getGroupPolicyAddress(group);
+ const policyThreshold = getGroupPolicyThreshold(group);
```

These utility functions could be implemented in a separate file:

```typescript
export function getGroupPolicyAddress(group?: ExtendedGroupType): string {
  return group?.policies[0]?.address ?? '';
}

export function getGroupPolicyThreshold(group?: ExtendedGroupType): string {
  return (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 29d4974d5a1a967c497ee9b5935921777017f24d and 359721cf1e82289312c1930f533d8cf294373c65.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `components/groups/components/Proposals.tsx` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>components/groups/components/Proposals.tsx (1)</summary>

```
Learnt from: chalabi2
PR: liftedinit/manifest-app#9
File: components/groups/components/groupProposals.tsx:207-208
Timestamp: 2025-03-12T20:27:10.521Z
Learning: In the `GroupProposals` component, redundant checks for `group.policies.length > 0` are unnecessary because prior validations ensure that `group.policies[0]` is safe to access.
```

</details>

</details><details>
<summary>🪛 GitHub Check: codecov/patch</summary>

<details>
<summary>components/groups/components/Proposals.tsx</summary>

[warning] 141-141: components/groups/components/Proposals.tsx#L141
Added line #L141 was not covered by tests

---

[warning] 162-163: components/groups/components/Proposals.tsx#L162-L163
Added lines #L162 - L163 were not covered by tests

---

[warning] 167-170: components/groups/components/Proposals.tsx#L167-L170
Added lines #L167 - L170 were not covered by tests

---

[warning] 179-180: components/groups/components/Proposals.tsx#L179-L180
Added lines #L179 - L180 were not covered by tests

---

[warning] 183-184: components/groups/components/Proposals.tsx#L183-L184
Added lines #L183 - L184 were not covered by tests

---

[warning] 186-194: components/groups/components/Proposals.tsx#L186-L194
Added lines #L186 - L194 were not covered by tests

---

[warning] 196-196: components/groups/components/Proposals.tsx#L196
Added line #L196 was not covered by tests

---

[warning] 203-214: components/groups/components/Proposals.tsx#L203-L214
Added lines #L203 - L214 were not covered by tests

---

[warning] 244-246: components/groups/components/Proposals.tsx#L244-L246
Added lines #L244 - L246 were not covered by tests

</details>

</details>

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (4)</summary><blockquote>

<details>
<summary>components/groups/components/Proposals.tsx (4)</summary>

`73-78`: **Good use of memoization to prevent unnecessary recalculations.**

Using `useMemo` here is appropriate as it prevents recalculating the proposal-tally mapping on every render, avoiding potential O(n²) performance issues.

---

`186-196`: **Status determination logic needs test coverage.**

This critical branch logic determines proposal status based on voting results but lacks test coverage as indicated by previous reviews and static analysis.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 186-194: components/groups/components/Proposals.tsx#L186-L194
Added lines #L186 - L194 were not covered by tests

---

[warning] 196-196: components/groups/components/Proposals.tsx#L196
Added line #L196 was not covered by tests

</details>

</details>

---

`246-247`: **Good solution for removing proposalId from query.**

Using object destructuring to remove the proposalId from the router query is a clean approach and avoids the performance concerns associated with using `delete` on objects.

---

`19-46`: <details>
<summary>✅ Verification successful</summary>

**The `isProposalPassing` function looks solid but needs test coverage.**

The implementation correctly uses BigInt for vote calculations and handles threshold verification and tie detection. The return value provides a comprehensive breakdown of the voting state.


As indicated in previous reviews, this critical function still lacks test coverage. Consider adding unit tests covering different scenarios such as:
- When threshold is reached/not reached
- When there's a tie
- When yes votes exceed no votes
- When yes votes are less than no votes
- Edge cases with zero votes

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if any tests have been added for isProposalPassing since the previous review
rg -A 3 "isProposalPassing" --glob="**/*.test.{ts,tsx}" components/groups/
```

Length of output: 2677

---



</details>

**Review Update: Test Coverage Verified for `isProposalPassing`**

The added tests in `components/groups/components/__tests__/Proposals.test.tsx` now cover the critical scenarios:
- When the vote threshold is reached versus not reached
- Handling undefined tally inputs (edge case with zero votes)
- Comparing yes votes against no votes in both directions
- Detecting tie conditions

The function implementation and its testing now look solid. Please proceed with these changes.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T04:18:41Z
user: login: fmorency
body: Thanks!

- Submitting a tx and clicking on "View Proposal" in the toast doesn't work anymore
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T15:02:47Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 3**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>components/groups/components/Proposals.tsx (1)</summary><blockquote>

`232-234`: **Add a safety check before accessing message type**

The code assumes that proposal messages have a specific structure. Despite the provided learning that "all proposals have at least one message," it's still safer to add explicit validation.

```diff
-{proposal.messages.length > 0
-  ? getHumanReadableType((proposal.messages[0] as any)['@type'])
-  : 'No messages'}
+{proposal.messages.length > 0 && proposal.messages[0] && (proposal.messages[0] as any)['@type']
+  ? getHumanReadableType((proposal.messages[0] as any)['@type'])
+  : 'No messages'}
```

This change ensures the code is resilient even if the message structure changes.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 359721cf1e82289312c1930f533d8cf294373c65 and 20079760eb1a10f968f457a15dee6949ce70198a.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `components/groups/components/__tests__/__snapshots__/groupControls.test.tsx.snap` is excluded by `!**/*.snap`

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `components/bank/components/historyBox.tsx` (1 hunks)
* `components/bank/components/tokenList.tsx` (1 hunks)
* `components/factory/components/DenomList.tsx` (1 hunks)
* `components/groups/components/Proposals.tsx` (1 hunks)
* `components/groups/components/__tests__/groupControls.test.tsx` (1 hunks)
* `components/groups/components/__tests__/groupProposals.test.tsx` (5 hunks)
* `components/groups/components/groupControls.tsx` (8 hunks)
* `components/groups/components/myGroups.tsx` (10 hunks)
* `tests/mock.ts` (1 hunks)

</details>

<details>
<summary>✅ Files skipped from review due to trivial changes (2)</summary>

* components/bank/components/historyBox.tsx
* components/bank/components/tokenList.tsx

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (1)</summary>

* components/groups/components/myGroups.tsx

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧠 Learnings (1)</summary>

<details>
<summary>components/groups/components/Proposals.tsx (2)</summary>

```
Learnt from: chalabi2
PR: liftedinit/manifest-app#9
File: components/groups/components/groupProposals.tsx:207-208
Timestamp: 2025-03-12T20:27:10.521Z
Learning: In the `GroupProposals` component, redundant checks for `group.policies.length > 0` are unnecessary because prior validations ensure that `group.policies[0]` is safe to access.
```

```
Learnt from: chalabi2
PR: liftedinit/manifest-app#9
File: components/groups/components/groupProposals.tsx:373-373
Timestamp: 2025-03-12T20:27:10.521Z
Learning: In `components/groups/components/groupProposals.tsx`, all proposals have at least one message, so accessing `proposal.messages[0]` without a length check is safe.
```

</details>

</details><details>
<summary>🧬 Code Definitions (1)</summary>

<details>
<summary>components/groups/components/__tests__/groupProposals.test.tsx (1)</summary><blockquote>

<details>
<summary>components/groups/components/groupControls.tsx (1) (1)</summary>

* `GroupControls` (44:329)

</details>

</blockquote></details>

</details><details>
<summary>🪛 GitHub Check: build</summary>

<details>
<summary>components/groups/components/__tests__/groupControls.test.tsx</summary>

[failure] 110-110: error: expect(received).toMatchSnapshot(expected)
Expected: 
"<DocumentFragment>
  <div
    class="_14der0i88"
    data-interchain-ui-provider="true"
    data-is-controlled="false"
    style="visibility: visible;"
  />
  <div
    class=""
  >
    <div
      class="flex w-full h-full md:flex-row flex-col md:gap-8"
    >
      <div
        class="flex flex-col w-full md:w-[48%] h-full"
      >
        <div
          class="flex items-center justify-between mb-4"
        >
          <div
            class="flex items-center space-x-4"
          >
            <button
              aria-label="Go back to groups list"
              class="btn btn-circle rounded-[12px] bg-secondary btn-md focus:outline-hidden focus-visible:ring-1 focus-visible:ring-primary"
            >
              <svg
                class="text-primary"
                fill="none"
                height="24"
                viewBox="0 0 24 24"
                width="24"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M15.0898 4.0798L8.56984 10.5998C7.79984 11.3698 7.79984 12.6298 8.56984 13.3998L15.0898 19.9198"
                  stroke="currentColor"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-miterlimit="10"
                  stroke-width="2.25"
                />
              </svg>
            </button>
            <h1
              class="text-2xl font-bold text-primary-content truncate"
            >
              Untitled Group
            </h1>
            <div
              class=""
            >
              <img
                alt="Profile Avatar"
                class="rounded-md dark:bg-[#FFFFFF0F] bg-[#0000000A]"
                src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCcgc3R5bGU9J2JhY2tncm91bmQtY29sb3I6cmdiYSgyNTUsMjU1LDI1NSwwKTsnPjxnIHN0eWxlPSdmaWxsOnJnYmEoNTYsMTIsMTk3LDEpOyBzdHJva2U6cmdiYSg1NiwxMiwxOTcsMSk7IHN0cm9rZS13aWR0aDowLjI7Jz48cmVjdCAgeD0nMTgnIHk9JzEwJyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzE4JyB5PScxNCcgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScxOCcgeT0nMjInIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMTgnIHk9JzI2JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzE0JyB5PScxMCcgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScyMicgeT0nMTAnIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMTQnIHk9JzE0JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzIyJyB5PScxNCcgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScxNCcgeT0nMTgnIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMjInIHk9JzE4JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzE0JyB5PScyMicgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScyMicgeT0nMjInIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMTQnIHk9JzI2JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzIyJyB5PScyNicgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScxMCcgeT0nMjYnIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMjYnIHk9JzI2JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PC9nPjwvc3ZnPg=="
                style="height: 40px; width: 40px;"
              />
            </div>
          </div>
        </div>
      </div>
    </div>
    <div
      aria-orientation="horizontal"
      class="flex flex-row tabs tabs-border tabs-lg"
      role="tablist"
    >
      <button
        aria-controls="headlessui-tabs-panel-:rdh:"
        aria-selected="false"
        class="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1"
        data-headlessui-state=""
        id="headlessui-tabs-tab-:rdd:"
        role="tab"
        tabindex="-1"
        type="button"
      >
        Proposals
      </button>
      <button
        aria-controls="headlessui-tabs-panel-:rdi:"
        aria-selected="false"
        class="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1"
        data-headlessui-state=""
        id="headlessui-tabs-tab-:rde:"
        role="tab"
        tabindex="-1"
        type="button"
      >
        Assets
      </button>
      <button
        aria-controls="headlessui-tabs-panel-:rdj:"
        aria-selected="false"
        class="font-bold ta

</details>

</details>
<details>
<summary>🪛 GitHub Actions: Test and Coverage</summary>

<details>
<summary>components/groups/components/__tests__/groupControls.test.tsx</summary>

[error] 110-110: expect(received).toMatchSnapshot(expected) failed. The received output does not match the expected snapshot.

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (15)</summary><blockquote>

<details>
<summary>components/factory/components/DenomList.tsx (1)</summary>

`203-203`: **Added test identifier to improve component testability.**

Adding the `data-testid` attribute to the root div element enhances the component's testability by providing a specific identifier for testing frameworks.

</details>
<details>
<summary>tests/mock.ts (1)</summary>

`70-76`: **Improved flexibility of router mocking.**

The `mockRouter` function now accepts an optional `routerConfig` parameter, allowing for more customizable router mocks during testing. This enhancement enables testing components with different router states and query parameters.

</details>
<details>
<summary>components/groups/components/__tests__/groupControls.test.tsx (1)</summary>

`113-139`: **Good implementation of router testing.**

This test correctly validates that the component responds to routing changes, particularly when a specific tab is selected via query parameters. The assertion on line 134-138 validates that the router's push method is called with the correct parameters.

</details>
<details>
<summary>components/groups/components/groupControls.tsx (5)</summary>

`24-26`: **Good refactoring of component props.**

Replacing the `policyAddress` prop with a more comprehensive `group` prop of type `ExtendedGroupType` allows the component to access more group information directly, improving maintainability and reducing prop drilling.

---

`61-61`: **Robust null handling with optional chaining.**

The implementation correctly handles potential undefined values by using optional chaining (`?.`) for both `group` and `policies`, addressing the previous TypeError mentioned in past review comments.

---

`185-199`: **Enhanced tab management with headlessui/react.**

Replacing the manual tab management with the `Tab` component from `@headlessui/react` simplifies the code and provides built-in accessibility features. The `ui-selected:tab-active` class utilizes the component's state for styling.

---

`216-222`: **Encapsulated proposal rendering in dedicated component.**

Moving the proposal rendering logic to a dedicated `GroupProposals` component improves code organization and maintainability, aligning with the PR objective to move all proposals logic into a dedicated component.

---

`315-324`: **Verify the negative margin impact.**

The Tab.Panel for Tokens includes a negative top margin (`-mt-7`), which might cause layout issues depending on the context.

Please confirm that this negative margin doesn't cause any visual issues across different viewport sizes and that it's intentional for the design layout.

</details>
<details>
<summary>components/groups/components/Proposals.tsx (4)</summary>

`146-146`: **Fix missing optional chaining for group.policies**

Need to add optional chaining for array access.

```diff
-const policyAddress = group?.policies[0]?.address ?? '';
+const policyAddress = group?.policies?.[0]?.address ?? '';
```

Based on previous learnings, prior validations might ensure `group.policies[0]` is safe to access, but explicit optional chaining is safer for maintainability.

---

`247-248`: **Good job using object destructuring instead of delete**

This approach of using object destructuring to remove a property is more performant than using the `delete` operator.

```javascript
const { proposalId: _, ...query } = router.query;
router.push({ pathname: router.pathname, query }, undefined, { shallow: true });
```

---

`19-47`: <details>
<summary>✅ Verification successful</summary>

**Well-implemented vote calculation but needs test coverage**

The `isProposalPassing` function correctly uses BigInt for vote calculations (important for blockchain applications), and properly handles various scenarios including threshold checks and tie detection.

However, this function is critical to the app's correctness and lacks test coverage according to static analysis.

Consider adding comprehensive unit tests that verify:
- Threshold reaching calculations 
- Tie detection
- Various vote distribution scenarios

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for existing tests covering isProposalPassing function
rg -A 3 "isProposalPassing" components/groups/components/__tests__
```

Length of output: 2669

---



</details>

**Test coverage confirmed for isProposalPassing**  
After verification, the test suite located at `components/groups/components/__tests__/Proposals.test.tsx` confirms comprehensive unit tests for the `isProposalPassing` function. The tests cover threshold calculations, tie detection, and various vote distribution scenarios. No further test cases are required at this time.

---

`175-198`: <details>
<summary>❓ Verification inconclusive</summary>

**Add test coverage for proposal status determination logic**

The status determination logic handles multiple cases (Pending, Failure, Execute, Passing, Failing, Tie) but lacks test coverage. These critical branches need validation.

Consider adding unit tests that verify all possible status outcomes:
- When `isThresholdReached` is true and `isTie` is true (resulting in 'Tie')
- When `isThresholdReached` is true and `isTie` is false (resulting in 'Passing' or 'Failing')
- When proposal status is 'PROPOSAL_STATUS_ACCEPTED'
- When executor result is 'PROPOSAL_EXECUTOR_RESULT_FAILURE'

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for existing tests that might cover these branches
rg -A 5 "status = 'Passing'" --glob="**/*.test.tsx" components/groups/
```

Length of output: 78

---



</details>

**Add Comprehensive Unit Tests for Proposal Status Determination**

The current logic in `components/groups/components/Proposals.tsx` sets the proposal status based on several branches—defaulting to "Pending" but potentially updating to "Failure", "Execute", "Tie", "Passing", or "Failing" depending on the proposal’s executor result, status, and tally evaluations. An initial search for tests covering these branches (e.g. looking for occurrences of `"status = 'Passing'"` in test files under the components/groups directory) returned no results, which suggests that this critical logic isn’t exercised by existing tests.

Please add unit tests that comprehensively verify all the following cases:
- **Executor Failure:** When `proposal.executor_result` equals the JSON representation of `PROPOSAL_EXECUTOR_RESULT_FAILURE`, the status should be `"Failure"`.
- **Accepted Proposal:** When `proposal.status` equals the JSON representation of `PROPOSAL_STATUS_ACCEPTED`, the status should be `"Execute"`.
- **Tally Branch:**
  - If `isThresholdReached` is true and `isTie` is true, the status should be `"Tie"`.
  - If `isThresholdReached` is true and `isTie` is false, the status should be set to `"Passing"` when `isPassing` is true, or `"Failing"` when `isPassing` is false.
- **Default Case:** When none of the conditions apply, ensure that the status remains `"Pending"`.

Since the automated search was inconclusive, please manually verify that no existing tests address this logic and proceed with adding the necessary test coverage.

</details>
<details>
<summary>components/groups/components/__tests__/groupProposals.test.tsx (3)</summary>

`11-34`: **Good refactoring of test props to match component API changes**

The test properly updates from using a direct `policyAddress` prop to a `group` object with policies, aligning with the component's refactored API. This ensures tests reflect the actual component structure.

---

`5-5`: **Updated component name to GroupControls**

The test correctly updates the component references from the previous implementation to the new `GroupControls` component, ensuring tests target the right component.



Also applies to: 96-96, 109-109, 122-122, 135-135

---

`1-2`: **Good test setup with proper testing utilities**

The test file uses appropriate testing utilities from `@testing-library/react` and `bun:test`, which is good practice for React component testing.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T17:29:34Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>🧹 Nitpick comments (1)</summary><blockquote>

<details>
<summary>components/groups/components/__tests__/groupControls.test.tsx (1)</summary><blockquote>

`16-25`: **Avoid using `as any` for mock objects.**

In the mock props, TypeScript type safety is bypassed with `as any`. While this works for testing, it's better to properly type your mocks to catch potential type-related issues early and improve maintainability.

```diff
- } as any,
+ } as PolicySDKType, // or appropriate type
- } as any,
+ } as ExtendedGroupType, // or appropriate type
```

Consider creating proper type definitions for your test objects or importing the actual types from your codebase.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 20079760eb1a10f968f457a15dee6949ce70198a and 8728afd584716d89866b4ef55ff6b70f4f7c630e.

</details>

<details>
<summary>📒 Files selected for processing (1)</summary>

* `components/groups/components/__tests__/groupControls.test.tsx` (1 hunks)

</details>

<details>
<summary>⏰ Context from checks skipped due to timeout of 90000ms (1)</summary>

* GitHub Check: build

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>components/groups/components/__tests__/groupControls.test.tsx (3)</summary>

`62-109`: **Well-structured test with comprehensive tab validation.**

This test thoroughly verifies tab rendering and switching functionality. The approach of checking both the presence of tabs and the correct content display after tab switches is excellent. The code is well-organized with clear comments explaining the test steps.

---

`111-137`: **Good test for routing functionality.**

The router testing is well-implemented with proper setup of initial query parameters and verification of router updates. The test correctly checks both DOM changes and router method calls.

---

`1-138`: **Fix failing snapshot test.**

The test is well-structured and correctly validates tab navigation functionality, but the pipeline indicates that the snapshot test is failing, as the rendered output doesn't match the expected snapshot.

Update the snapshot by running the tests with the snapshot update flag:

```shell
#!/bin/bash
# Run tests with snapshot update flag
bun test --update-snapshots
```

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T20:06:31Z
user: login: fmorency
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T12:44:17Z

Review comments:
diff_hunk: @@ -525,7 +486,42 @@ export default React.memo(function YourGroups({
       </div>
     </div>
   );
-});
+};
+
+const LoadingGroupRow = ({ key }: { key: React.Key }) => {
+  return (
+    <tr key={key} data-testid="skeleton-row">
+      <td className="bg-secondary rounded-l-[12px] ">
+        <div className="flex items-center space-x-3">
+          <div className="skeleton w-11 h-11 rounded-md shrink-0"></div>
+          <div className="skeleton h-3 w-24"></div>
+        </div>
+      </td>
+      <td className="bg-secondary hidden xl:table-cell">
+        <div className="skeleton h-2 w-8"></div>
+      </td>
+      <td className="bg-secondary hidden sm:table-cell ">
+        <div className="skeleton h-2 w-16"></div>
+      </td>
+      <td className="bg-secondary hidden xl:table-cell">
+        <div className="skeleton h-2 w-20"></div>
+      </td>
+      <td className="bg-secondary hidden lg:table-cell">
+        <div className="skeleton h-2 w-32"></div>
+      </td>
+      <td className="bg-secondary rounded-r-[12px] w-1/6 hidden xs:table-cell">
+        <div className="flex space-x-2 justify-end">
+          <button className="btn btn-md btn-outline btn-square btn-info" disabled>
+            <PiInfo className="w-7 h-7 text-current opacity-50" />
+          </button>
+          <button className="btn btn-md btn-outline btn-square btn-primary" disabled>
+            <MemberIcon className="w-7 h-7 text-current opacity-50" />
+          </button>
+        </div>
+      </td>
+    </tr>
+  );
+};
path: components/groups/components/myGroups.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**React key caveat.**

Destructuring `key` from props has no effect, since React doesn’t pass `key` as a normal prop. Consider removing the `key` prop from the function’s parameters and using an internal ID if needed. Otherwise, the parent’s `<LoadingGroupRow key={index}>` suffices.

```diff
- const LoadingGroupRow = ({ key }: { key: React.Key }) => {
+ const LoadingGroupRow = () => {
    return (
-     <tr key={key} data-testid="skeleton-row">
+     <tr data-testid="skeleton-row">
        ...
     </tr>
    );
  };
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
const LoadingGroupRow = () => {
  return (
    <tr data-testid="skeleton-row">
      <td className="bg-secondary rounded-l-[12px] ">
        <div className="flex items-center space-x-3">
          <div className="skeleton w-11 h-11 rounded-md shrink-0"></div>
          <div className="skeleton h-3 w-24"></div>
        </div>
      </td>
      <td className="bg-secondary hidden xl:table-cell">
        <div className="skeleton h-2 w-8"></div>
      </td>
      <td className="bg-secondary hidden sm:table-cell ">
        <div className="skeleton h-2 w-16"></div>
      </td>
      <td className="bg-secondary hidden xl:table-cell">
        <div className="skeleton h-2 w-20"></div>
      </td>
      <td className="bg-secondary hidden lg:table-cell">
        <div className="skeleton h-2 w-32"></div>
      </td>
      <td className="bg-secondary rounded-r-[12px] w-1/6 hidden xs:table-cell">
        <div className="flex space-x-2 justify-end">
          <button className="btn btn-md btn-outline btn-square btn-info" disabled>
            <PiInfo className="w-7 h-7 text-current opacity-50" />
          </button>
          <button className="btn btn-md btn-outline btn-square btn-primary" disabled>
            <MemberIcon className="w-7 h-7 text-current opacity-50" />
          </button>
        </div>
      </td>
    </tr>
  );
};
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Fixed in commit 6c27781
created_at: 2025-03-13T18:27:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -63,15 +59,13 @@ export default function GroupControls({
   skeletonTxCount,
   proposalPageSize = 10,
 }: GroupControlsProps) {
+  const policyAddress = group.policies[0]?.address ?? '';
+
path: components/groups/components/groupControls.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix potential TypeError with group.policies**

The pipeline failure indicates that `group.policies` can be undefined. While you've used optional chaining for `policies[0]?.address`, you need to handle the case where `group` itself might be undefined.


Update the line to handle both cases:

```diff
- const policyAddress = group.policies[0]?.address ?? '';
+ const policyAddress = group?.policies?.[0]?.address ?? '';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const policyAddress = group?.policies?.[0]?.address ?? '';
`````

</details>

<!-- suggestion_end -->

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Actions: Test and Coverage</summary>

[error] 62-62: TypeError: undefined is not an object (evaluating 'group.policies')

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Fixed in commit 6c27781
created_at: 2025-03-13T18:27:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,247 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+function isProposalPassing(tally: QueryTallyResultResponseSDKType, policyThreshold: number) {
+  const yesCount = BigInt(tally?.tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposals.map(proposal => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tallies.find(t => t.proposalId === proposal.id)?.tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
+
+  let status = 'Pending';
+  // We need to compare strings here
+  if (
+    proposal.executor_result.toString() ===
+    proposalExecutorResultToJSON(ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE)
+  ) {
+    status = 'Failure';
+  } else if (
+    proposal.status.toString() === proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
+  ) {
+    status = 'Execute';
+  } else if (tally) {
+    const { isPassing, isThresholdReached, isTie } = isProposalPassing(
+      tally,
+      Number.parseInt(policyThreshold) || 0
+    );
+    if (isThresholdReached) {
+      if (isTie) {
+        status = 'Tie';
+      } else {
+        status = isPassing ? 'Passing' : 'Failing';
+      }
+    }
+  }
+
+  return (
+    <>
+      <tr
+        key={proposal.id.toString()}
+        onClick={() => {
+          setShowVoteModal(true);
+          // Update URL without navigating
+          router.push(
+            `/groups?policyAddress=${policyAddress}&proposalId=${proposal.id}`,
+            undefined,
+            {
+              shallow: true,
+            }
+          );
+        }}
+        className="group text-black dark:text-white rounded-lg cursor-pointer"
+      >
+        <td className="bg-secondary group-hover:bg-base-300 rounded-l-[12px] px-4 py-4 w-[25%]">
+          {proposal.id.toString()}
+        </td>
+        <td
+          className={`bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:rounded-none xxs:rounded-r-[12px] xs:rounded-r-[12px] xl:rounded-r-none`}
+        >
+          {proposal.title}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] hidden xl:table-cell">
+          {timeLeft}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:table-cell md:hidden hidden xl:table-cell ">
+          {proposal.messages.length > 0
+            ? getHumanReadableType((proposal.messages[0] as any)['@type'])
+            : 'No messages'}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 rounded-r-[12px] sm:table-cell xxs:hidden hidden 2xl:table-cell">
+          {!tally ? <span className="loading loading-spinner loading-xs"></span> : status}
+
+          {
+            // Show vote modal if the proposal is selected, cannot be outside `<tr>`.
+            showVoteModal && (
+              <VoteDetailsModal
+                policyAddress={policyAddress}
+                proposalId={proposal.id}
+                onClose={() => {
+                  setShowVoteModal(false);
+                  const query = { ...router.query };
+                  delete query.proposalId;
+                  router.push(
+                    {
+                      pathname: router.pathname,
+                      query,
+                    },
+                    undefined,
+                    { shallow: true }
+                  );
+                }}
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid using `delete` to remove query properties.**

Using `delete query.proposalId` can cause performance flags. Instead, set it to `undefined` or construct a new object without `proposalId`. For example:
```diff
- delete query.proposalId
+ const { proposalId, ...rest } = router.query
+ router.push(
+   { pathname: router.pathname, query: rest },
+   undefined,
+   { shallow: true }
+ )
```



> Committable suggestion skipped: line range outside the PR's diff.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 229-229: Avoid the delete operator which can impact performance.

Unsafe fix: Use an undefined assignment instead.


(lint/performance/noDelete)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-13T18:50:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+function isProposalPassing(tally: QueryTallyResultResponseSDKType, policyThreshold: number) {
+  const yesCount = BigInt(tally?.tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposals.map(proposal => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tallies.find(t => t.proposalId === proposal.id)?.tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group.policies[0]?.address ?? '';
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix missing optional chaining for group.policies**

Unlike in `groupControls.tsx`, this line doesn't use optional chaining for `group.policies`, which could cause issues if `group` is undefined or `policies` is not an array.

```diff
-const policyAddress = group.policies[0]?.address ?? '';
+const policyAddress = group?.policies?.[0]?.address ?? '';
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  const policyAddress = group?.policies?.[0]?.address ?? '';
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->

✅ Addressed in commit 29d4974
created_at: 2025-03-14T00:45:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,239 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+function isProposalPassing(tally: QueryTallyResultResponseSDKType, policyThreshold: number) {
+  const yesCount = BigInt(tally?.tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Well-implemented vote calculation with BigInt**

Using BigInt for vote calculations is appropriate for handling potentially large numbers in blockchain applications. The function covers different voting scenarios including tie detection.


This critical function lacks test coverage according to static analysis. Consider adding unit tests:

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any tests for the isProposalPassing function
rg -A 3 "isProposalPassing" components/groups/components/__tests__
```

Length of output: 66

---



</details>

**Action Required: Add Unit Tests for `isProposalPassing`**

The implementation of `isProposalPassing` is solid in its use of BigInt for vote calculations and correctly handles tie detection and threshold logic. However, our static analysis indicates that there are currently no unit tests covering this critical function (a search in the `components/groups/components/__tests__` folder returned no results). It’s important to add tests to validate scenarios such as when the threshold is reached, tie conditions, and various vote distributions.

- **File:** `components/groups/components/Proposals.tsx` (lines 18–46)
- **Suggested Action:** Add unit tests in an appropriate test file (e.g., within `components/groups/components/__tests__/`) to ensure all branches and edge cases of `isProposalPassing` are properly exercised.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 18-44: components/groups/components/Proposals.tsx#L18-L44
Added lines #L18 - L44 were not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T00:45:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -132,24 +129,16 @@
     if (policyAddress && typeof policyAddress === 'string') {
       const group = groups.groups.find(g => g.policies?.[0]?.address === policyAddress);
       if (group) {
-        let groupName = 'Untitled Group';
-        try {
-          const metadata = group.metadata ? JSON.parse(group.metadata) : null;
-          groupName = metadata?.title ?? 'Untitled Group';
-        } catch (e) {
-          // If JSON parsing fails, fall back to default name
-          // console.warn('Failed to parse group metadata:', e);
-        }
-
-        setSelectedGroupName(groupName);
         setSelectedGroup(group);
       } else {
         // Group not found, reset selected group.
         setSelectedGroup(null);
-        router.push('/groups', undefined, { shallow: true });
+        if (!isLoading) {
+          router.push('/groups', undefined, { shallow: true });
+        }
path: components/groups/components/myGroups.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Good fix to prevent unnecessary redirects during loading.**

Adding a check for `isLoading` before redirecting is a sensible improvement that prevents navigation during the loading state. Also good that you've included `isLoading` in the dependency array to ensure the effect runs when the loading state changes.


The static analysis indicates these lines aren't covered by tests. Consider adding a test case that verifies this behavior:




Also applies to: 141-141

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for test coverage of the router push condition with isLoading

# Find any tests that check the router push behavior with isLoading
rg -A 5 -B 5 "isLoading.*router.push|router.push.*isLoading" --glob "**/*.test.{tsx,ts,jsx,js}"
```

Length of output: 97

---



</details>

**Approved fix; however, additional test coverage is needed.**

Great improvement—the check for `isLoading` successfully prevents unnecessary redirects during loading. However, our static analysis did not locate any tests covering the router push behavior when `isLoading` is involved. Please add tests to ensure that:

- The redirect (`router.push('/groups', undefined, { shallow: true })`) only occurs when `isLoading` is false.
- This behavior is verified across the relevant sections (lines 136–138 and line 141).

For example, you might add a test case similar to the following shell script snippet as part of your testing effort:

```shell
#!/bin/bash
# Suggested test coverage check for the router push condition with isLoading

rg -A 5 -B 5 "isLoading.*router.push|router.push.*isLoading" --glob "**/*.test.{tsx,ts,jsx,js}"
```

Once the new tests are in place, we can confirm that the redirection logic works as intended under various loading states.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 136-138: components/groups/components/myGroups.tsx#L136-L138
Added lines #L136 - L138 were not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T04:13:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,243 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+export function isProposalPassing(tally: TallyResultSDKType | undefined, policyThreshold: number) {
+  const yesCount = BigInt(tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposals.map(proposal => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tallies.find(t => t.proposalId === proposal.id)?.tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group?: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group?.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
+
+  let status = 'Pending';
+  // We need to compare strings here
+  if (
+    proposal.executor_result.toString() ===
+    proposalExecutorResultToJSON(ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE)
+  ) {
+    status = 'Failure';
+  } else if (
+    proposal.status.toString() === proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
+  ) {
+    status = 'Execute';
+  } else if (tally) {
+    const { isPassing, isThresholdReached, isTie } = isProposalPassing(
+      tally?.tally,
+      Number.parseInt(policyThreshold) || 0
+    );
+    if (isThresholdReached) {
+      if (isTie) {
+        status = 'Tie';
+      } else {
+        status = isPassing ? 'Passing' : 'Failing';
+      }
+    }
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Add test coverage for proposal status determination logic**

The static analysis indicates that these lines aren't covered by tests. Consider adding test coverage for the status determination logic, especially for different proposal states: passing, failing, tie, etc.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check for existing tests that might cover these branches
rg -A 5 "status = 'Passing'" --glob="**/*.test.tsx" components/groups/
```

Length of output: 78

---



</details>

**Action Required: Add comprehensive test coverage for proposal status determination logic.**

The current search did not reveal any tests exercising the branch logic in `components/groups/components/Proposals.tsx` (lines 172–182). Please add unit tests to cover the following scenarios:
  
- When `isThresholdReached` is true and `isTie` is true (should result in a status of 'Tie').
- When `isThresholdReached` is true and `isTie` is false, verifying both the passing case (status 'Passing') and the failing case (status 'Failing').
- Edge cases for variations in the `policyThreshold` value (e.g., when parsing fails or returns 0).

This will ensure that all possible proposal state outcomes are properly validated.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 172-180: components/groups/components/Proposals.tsx#L172-L180
Added lines #L172 - L180 were not covered by tests

---

[warning] 182-182: components/groups/components/Proposals.tsx#L182
Added line #L182 was not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T04:13:57Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,257 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+export function isProposalPassing(tally: TallyResultSDKType | undefined, policyThreshold: number) {
+  const yesCount = BigInt(tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+/**
+ * GroupProposals component displays a list of proposals for a group in a table.
+ * @param group The group to display proposals for.
+ * @param proposals The list of proposals to display.
+ * @constructor
+ */
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  // This is O(n^2), even if we don't expect a lot of proposals, let's memoize it.
+  const proposalTallies = React.useMemo(() => {
+    return proposals.map(proposal => ({
+      proposal,
+      tally: tallies.find(t => t.proposalId === proposal.id)?.tally,
+    }));
+  }, [proposals, tallies]);
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposalTallies.map(({ proposal, tally }) => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group?: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group?.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
+
+  let status = 'Pending';
+  // We need to compare strings here
+  if (
+    proposal.executor_result.toString() ===
+    proposalExecutorResultToJSON(ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE)
+  ) {
+    status = 'Failure';
+  } else if (
+    proposal.status.toString() === proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
+  ) {
+    status = 'Execute';
+  } else if (tally) {
+    const { isPassing, isThresholdReached, isTie } = isProposalPassing(
+      tally?.tally,
+      Number.parseInt(policyThreshold) || 0
+    );
+    if (isThresholdReached) {
+      if (isTie) {
+        status = 'Tie';
+      } else {
+        status = isPassing ? 'Passing' : 'Failing';
+      }
+    }
+  }
+
+  return (
+    <>
+      <tr
+        key={proposal.id.toString()}
+        onClick={() => {
+          setShowVoteModal(true);
+          // Update URL without navigating
+          router.push(
+            {
+              pathname: router.pathname,
+              query: { ...router.query, proposalId: proposal.id.toString() },
+            },
+            undefined,
+            {
+              shallow: true,
+            }
+          );
+        }}
+        className="group text-black dark:text-white rounded-lg cursor-pointer"
+      >
+        <td className="bg-secondary group-hover:bg-base-300 rounded-l-[12px] px-4 py-4 w-[25%]">
+          {proposal.id.toString()}
+        </td>
+        <td
+          className={`bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:rounded-none xxs:rounded-r-[12px] xs:rounded-r-[12px] xl:rounded-r-none`}
+        >
+          {proposal.title}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] hidden xl:table-cell">
+          {timeLeft}
+        </td>
+        <td className="bg-secondary group-hover:bg-base-300 px-4 py-4 w-[25%] sm:table-cell md:hidden hidden xl:table-cell ">
+          {proposal.messages.length > 0
+            ? getHumanReadableType((proposal.messages[0] as any)['@type'])
+            : 'No messages'}
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Add a safety check before accessing message type.**

The code assumes that the first message exists and has a specific structure. Add validation to prevent errors for proposals with empty messages.


```diff
- {proposal.messages.length > 0
-   ? getHumanReadableType((proposal.messages[0] as any)['@type'])
-   : 'No messages'}
+ {proposal.messages.length > 0 && proposal.messages[0] && (proposal.messages[0] as any)['@type']
+   ? getHumanReadableType((proposal.messages[0] as any)['@type'])
+   : 'No messages'}
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
            {proposal.messages.length > 0 && proposal.messages[0] && (proposal.messages[0] as any)['@type']
              ? getHumanReadableType((proposal.messages[0] as any)['@type'])
              : 'No messages'}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T04:18:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,257 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+export function isProposalPassing(tally: TallyResultSDKType | undefined, policyThreshold: number) {
+  const yesCount = BigInt(tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+/**
+ * GroupProposals component displays a list of proposals for a group in a table.
+ * @param group The group to display proposals for.
+ * @param proposals The list of proposals to display.
+ * @constructor
+ */
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  // This is O(n^2), even if we don't expect a lot of proposals, let's memoize it.
+  const proposalTallies = React.useMemo(() => {
+    return proposals.map(proposal => ({
+      proposal,
+      tally: tallies.find(t => t.proposalId === proposal.id)?.tally,
+    }));
+  }, [proposals, tallies]);
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposalTallies.map(({ proposal, tally }) => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group?: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group?.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
+
+  let status = 'Pending';
+  // We need to compare strings here
+  if (
+    proposal.executor_result.toString() ===
+    proposalExecutorResultToJSON(ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE)
+  ) {
+    status = 'Failure';
+  } else if (
+    proposal.status.toString() === proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
+  ) {
+    status = 'Execute';
+  } else if (tally) {
+    const { isPassing, isThresholdReached, isTie } = isProposalPassing(
+      tally?.tally,
+      Number.parseInt(policyThreshold) || 0
+    );
+    if (isThresholdReached) {
+      if (isTie) {
+        status = 'Tie';
+      } else {
+        status = isPassing ? 'Passing' : 'Failing';
+      }
+    }
+  }
+
+  return (
+    <>
+      <tr
+        key={proposal.id.toString()}
+        onClick={() => {
+          setShowVoteModal(true);
+          // Update URL without navigating
+          router.push(
+            {
+              pathname: router.pathname,
+              query: { ...router.query, proposalId: proposal.id.toString() },
+            },
+            undefined,
+            {
+              shallow: true,
+            }
+          );
+        }}
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Add test coverage for router navigation logic.**

The click handler logic updates the URL without navigating, but lacks test coverage according to static analysis.

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any tests covering the router push logic in ProposalRow
rg -A 3 "router.push" --glob="**/*.test.{ts,tsx}" components/groups/
```

Length of output: 693

---



</details>

**Action Required: Add test coverage for router navigation in Proposals.tsx**

The current onClick handler in `components/groups/components/Proposals.tsx` (lines 203–216) correctly updates the URL using `router.push` with shallow routing. However, while tests exist in `components/groups/components/__tests__/myGroups.test.tsx` verifying similar router push behavior for another component, there isn’t any coverage specifically ensuring that the proposals component’s navigation logic (i.e., adding `proposalId` to the query) works as intended.

- **Location needing attention:** `components/groups/components/Proposals.tsx` (lines 203–216)
- **Required checks:**
  - Verify that clicking triggers `setShowVoteModal(true)`.
  - Confirm that `router.push` is called with the current pathname and an updated query that includes `proposalId` (derived from `proposal.id.toString()`).
  - Ensure the navigation uses shallow routing.

Please add or update tests to cover this router navigation logic.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: codecov/patch</summary>

[warning] 203-214: components/groups/components/Proposals.tsx#L203-L214
Added lines #L203 - L214 were not covered by tests

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-14T04:18:40Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,140 @@
+import { ProposalSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { cleanup, fireEvent, screen } from '@testing-library/react';
+import { afterEach, describe, expect, jest, test } from 'bun:test';
+import React from 'react';
+
+import { GroupControls } from '@/components/groups/components/groupControls';
+import {
+  clearAllMocks,
+  formatComponent,
+  mockModule,
+  mockRouter,
+  renderWithChainProvider,
+} from '@/tests';
+
+const mockProps = {
+  group: {
+    policies: [
+      {
+        address: 'test_policy_address',
+        title: 'Test Policy',
+        description: 'Test Policy Description',
+      } as any,
+    ],
+  } as any,
+  isLoading: false,
+  txLoading: false,
+  onBack: jest.fn(),
+  currentPage: 0,
+  setCurrentPage: jest.fn(),
+  totalPages: 1,
+  sendTxs: [],
+  isError: false,
+  balances: [],
+  denoms: [],
+  pageSize: 6,
+  skeletonGroupCount: 0,
+  skeletonTxCount: 0,
+};
+
+const mockProposal: ProposalSDKType = {
+  executor_result: 2,
+  final_tally_result: {
+    yes_count: '1',
+    no_count: '2',
+    abstain_count: '3',
+    no_with_veto_count: '4',
+  },
+  group_policy_address: 'address1',
+  group_policy_version: 0n,
+  group_version: 0n,
+  id: 0n,
+  messages: [],
+  metadata: '',
+  proposers: [],
+  status: 2,
+  submit_time: new Date(),
+  summary: '',
+  title: '',
+  voting_period_end: new Date(),
+};
+
+describe('Tabs functionality', () => {
+  afterEach(() => {
+    cleanup();
+    clearAllMocks();
+  });
+
+  test('renders all tab options and switches between tabs correctly', async () => {
+    mockRouter();
+    mockModule.force('@/hooks/useQueries', () => ({
+      useProposalsByPolicyAccount: jest.fn().mockReturnValue({
+        proposals: [mockProposal],
+        isProposalsLoading: false,
+        isProposalsError: false,
+        refetchProposals: jest.fn(),
+      }),
+    }));
+
+    const mockup = renderWithChainProvider(<GroupControls {...mockProps} />);
+
+    // Ensure all tabs are present in the DOM
+    const proposalTab = screen.getByRole('tab', { name: /proposals/i });
+    const assetsTab = screen.getByRole('tab', { name: /assets/i });
+    const activityTab = screen.getByRole('tab', { name: /activity/i });
+    const tokensTab = screen.getByRole('tab', { name: /tokens/i });
+
+    expect(proposalTab).toBeInTheDocument();
+    expect(assetsTab).toBeInTheDocument();
+    expect(activityTab).toBeInTheDocument();
+    expect(tokensTab).toBeInTheDocument();
+
+    // Ensure the first tab is selected by default
+    expect(screen.queryByTestId('proposals')).toBeInTheDocument();
+
+    // Switch to "Assets" tab and verify the change
+    fireEvent.click(assetsTab);
+    expect(screen.queryByTestId('Group proposals')).not.toBeInTheDocument();
+    expect(screen.queryByTestId('tokenList')).toBeInTheDocument();
+
+    // Switch to "Activity" tab and verify the change
+    fireEvent.click(activityTab);
+    expect(screen.queryByTestId('tokenList')).not.toBeInTheDocument();
+    expect(screen.queryByTestId('historyBox')).toBeInTheDocument();
+
+    // Switch to "Tokens" tab and verify the change
+    fireEvent.click(tokensTab);
+    expect(screen.queryByTestId('historyBox')).not.toBeInTheDocument();
+    expect(screen.queryByTestId('denomList')).toBeInTheDocument();
+
+    expect(formatComponent(mockup.asFragment())).toMatchSnapshot();
+  });
path: components/groups/components/__tests__/groupControls.test.tsx
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix failing snapshot test.**

The test is well-structured and correctly validates tab navigation functionality, but the pipeline indicates that the snapshot test is failing, as the rendered output doesn't match the expected snapshot.

Update the snapshot by running the tests with the snapshot update flag:

```shell
#!/bin/bash
# Run tests with snapshot update flag
bun test --update-snapshots
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 GitHub Check: build</summary>

[failure] 110-110: error: expect(received).toMatchSnapshot(expected)
Expected: 
"<DocumentFragment>
  <div
    class="_14der0i88"
    data-interchain-ui-provider="true"
    data-is-controlled="false"
    style="visibility: visible;"
  />
  <div
    class=""
  >
    <div
      class="flex w-full h-full md:flex-row flex-col md:gap-8"
    >
      <div
        class="flex flex-col w-full md:w-[48%] h-full"
      >
        <div
          class="flex items-center justify-between mb-4"
        >
          <div
            class="flex items-center space-x-4"
          >
            <button
              aria-label="Go back to groups list"
              class="btn btn-circle rounded-[12px] bg-secondary btn-md focus:outline-hidden focus-visible:ring-1 focus-visible:ring-primary"
            >
              <svg
                class="text-primary"
                fill="none"
                height="24"
                viewBox="0 0 24 24"
                width="24"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  d="M15.0898 4.0798L8.56984 10.5998C7.79984 11.3698 7.79984 12.6298 8.56984 13.3998L15.0898 19.9198"
                  stroke="currentColor"
                  stroke-linecap="round"
                  stroke-linejoin="round"
                  stroke-miterlimit="10"
                  stroke-width="2.25"
                />
              </svg>
            </button>
            <h1
              class="text-2xl font-bold text-primary-content truncate"
            >
              Untitled Group
            </h1>
            <div
              class=""
            >
              <img
                alt="Profile Avatar"
                class="rounded-md dark:bg-[#FFFFFF0F] bg-[#0000000A]"
                src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc0MCcgaGVpZ2h0PSc0MCcgc3R5bGU9J2JhY2tncm91bmQtY29sb3I6cmdiYSgyNTUsMjU1LDI1NSwwKTsnPjxnIHN0eWxlPSdmaWxsOnJnYmEoNTYsMTIsMTk3LDEpOyBzdHJva2U6cmdiYSg1NiwxMiwxOTcsMSk7IHN0cm9rZS13aWR0aDowLjI7Jz48cmVjdCAgeD0nMTgnIHk9JzEwJyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzE4JyB5PScxNCcgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScxOCcgeT0nMjInIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMTgnIHk9JzI2JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzE0JyB5PScxMCcgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScyMicgeT0nMTAnIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMTQnIHk9JzE0JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzIyJyB5PScxNCcgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScxNCcgeT0nMTgnIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMjInIHk9JzE4JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzE0JyB5PScyMicgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScyMicgeT0nMjInIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMTQnIHk9JzI2JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PHJlY3QgIHg9JzIyJyB5PScyNicgd2lkdGg9JzQnIGhlaWdodD0nNCcvPjxyZWN0ICB4PScxMCcgeT0nMjYnIHdpZHRoPSc0JyBoZWlnaHQ9JzQnLz48cmVjdCAgeD0nMjYnIHk9JzI2JyB3aWR0aD0nNCcgaGVpZ2h0PSc0Jy8+PC9nPjwvc3ZnPg=="
                style="height: 40px; width: 40px;"
              />
            </div>
          </div>
        </div>
      </div>
    </div>
    <div
      aria-orientation="horizontal"
      class="flex flex-row tabs tabs-border tabs-lg"
      role="tablist"
    >
      <button
        aria-controls="headlessui-tabs-panel-:rdh:"
        aria-selected="false"
        class="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1"
        data-headlessui-state=""
        id="headlessui-tabs-tab-:rdd:"
        role="tab"
        tabindex="-1"
        type="button"
      >
        Proposals
      </button>
      <button
        aria-controls="headlessui-tabs-panel-:rdi:"
        aria-selected="false"
        class="font-bold tab ui-selected:tab-active focus:outline-1 focus:outline-primary focus:-outline-offset-1"
        data-headlessui-state=""
        id="headlessui-tabs-tab-:rde:"
        role="tab"
        tabindex="-1"
        type="button"
      >
        Assets
      </button>
      <button
        aria-controls="headlessui-tabs-panel-:rdj:"
        aria-selected="false"
        class="font-bold ta

</details>
<details>
<summary>🪛 GitHub Actions: Test and Coverage</summary>

[error] 110-110: expect(received).toMatchSnapshot(expected) failed. The received output does not match the expected snapshot.

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T17:29:33Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+export function isProposalPassing(tally: TallyResultSDKType | undefined, policyThreshold: number) {
+  const yesCount = BigInt(tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+/**
+ * GroupProposals component displays a list of proposals for a group in a table.
+ * @param group The group to display proposals for.
+ * @param proposals The list of proposals to display.
+ * @constructor
+ */
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  // This is O(n^2), even if we don't expect a lot of proposals, let's memoize it.
+  const proposalTallies = React.useMemo(() => {
+    return proposals.map(proposal => ({
+      proposal,
+      tally: tallies.find(t => t.proposalId === proposal.id)?.tally,
+    }));
+  }, [proposals, tallies]);
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+        data-testid="proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposalTallies.map(({ proposal, tally }) => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group?: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group?.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
+
+  let status = 'Pending';
+  // We need to compare strings here
+  if (
+    proposal.executor_result.toString() ===
+    proposalExecutorResultToJSON(ProposalExecutorResult.PROPOSAL_EXECUTOR_RESULT_FAILURE)
+  ) {
+    status = 'Failure';
+  } else if (
+    proposal.status.toString() === proposalStatusToJSON(ProposalStatus.PROPOSAL_STATUS_ACCEPTED)
+  ) {
+    status = 'Execute';
+  } else if (tally) {
+    const { isPassing, isThresholdReached, isTie } = isProposalPassing(
+      tally?.tally,
+      Number.parseInt(policyThreshold) || 0
+    );
+    if (isThresholdReached) {
+      if (isTie) {
+        status = 'Tie';
+      } else {
+        status = isPassing ? 'Passing' : 'Failing';
+      }
+    }
+  }
+
+  return (
+    <>
+      <tr
+        key={proposal.id.toString()}
+        onClick={() => {
+          setShowVoteModal(true);
+          // Update URL without navigating
+          router.push(
+            {
+              pathname: router.pathname,
+              query: { ...router.query, proposalId: proposal.id.toString() },
+            },
+            undefined,
+            {
+              shallow: true,
+            }
+          );
+        }}
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Add test coverage for router navigation logic**

The click handler correctly updates the URL without navigating (using shallow routing), but lacks test coverage.

Add tests that verify:
- Clicking triggers `setShowVoteModal(true)`
- URL is updated with the proposal ID
- Shallow routing is used

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any tests covering the router push logic
rg -A 3 "router.push" --glob="**/*.test.{ts,tsx}" components/groups/components/__tests__
```

Length of output: 1136

---



</details>

**Action Required: Add Dedicated Test Coverage for Proposals Router Navigation**

While tests exist for router navigation logic in other components (e.g., in _myGroups.test.tsx_ and _groupControls.test.tsx_), a search confirms that no tests currently cover the router push logic in the Proposals component. Please add tests that specifically verify in `components/groups/components/Proposals.tsx`:

- Clicking triggers `setShowVoteModal(true)`
- The URL is updated with the correct proposal ID (i.e., the query includes `proposalId: proposal.id.toString()`)
- Shallow routing is enforced by checking that the router is called with `{ shallow: true }`

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T17:29:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,258 @@
+import { QueryTallyResultResponseSDKType } from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/query';
+import {
+  ProposalSDKType,
+  ProposalStatus,
+  ThresholdDecisionPolicySDKType,
+  proposalStatusToJSON,
+} from '@liftedinit/manifestjs/dist/codegen/cosmos/group/v1/types';
+import { TallyResultSDKType } from '@liftedinit/manifestjs/src/codegen/cosmos/gov/v1/gov';
+import {
+  ProposalExecutorResult,
+  proposalExecutorResultToJSON,
+} from 'cosmjs-types/cosmos/group/v1/types';
+import { useRouter } from 'next/router';
+import React, { useEffect, useState } from 'react';
+
+import VoteDetailsModal from '@/components/groups/modals/voteDetailsModal';
+import { ExtendedGroupType, useMultipleTallyCounts } from '@/hooks';
+
+export function isProposalPassing(tally: TallyResultSDKType | undefined, policyThreshold: number) {
+  const yesCount = BigInt(tally?.yes_count ?? '0');
+  const noCount = BigInt(tally?.no_count ?? '0');
+  const noWithVetoCount = BigInt(tally?.no_with_veto_count ?? '0');
+  const abstainCount = BigInt(tally?.abstain_count ?? '0');
+
+  const totalVotes = yesCount + noCount + noWithVetoCount + abstainCount;
+  const totalNoVotes = noCount + noWithVetoCount;
+
+  // Check if threshold is reached
+  const threshold = BigInt(policyThreshold);
+  const isThresholdReached = totalVotes >= threshold;
+
+  // Check for tie
+  const isTie = yesCount === totalNoVotes && yesCount > 0;
+
+  // Determine if passing based on vote distribution
+  const isPassing = isThresholdReached && yesCount > totalNoVotes;
+
+  return {
+    isPassing,
+    yesCount,
+    noCount,
+    noWithVetoCount,
+    abstainCount,
+    isThresholdReached,
+    isTie,
+  };
+}
+
+function getHumanReadableType(type: string): string {
+  const parts = type.split('.');
+  const lastPart = parts[parts.length - 1];
+  return lastPart
+    .replace('Msg', '')
+    .replace(/([A-Z])/g, ' $1')
+    .trim();
+}
+
+export interface GroupProposalsProps {
+  group: ExtendedGroupType;
+  proposals: ProposalSDKType[];
+}
+
+/**
+ * GroupProposals component displays a list of proposals for a group in a table.
+ * @param group The group to display proposals for.
+ * @param proposals The list of proposals to display.
+ * @constructor
+ */
+export const GroupProposals = ({ group, proposals }: GroupProposalsProps) => {
+  const { tallies } = useMultipleTallyCounts(proposals.map(p => p.id));
+
+  // This is O(n^2), even if we don't expect a lot of proposals, let's memoize it.
+  const proposalTallies = React.useMemo(() => {
+    return proposals.map(proposal => ({
+      proposal,
+      tally: tallies.find(t => t.proposalId === proposal.id)?.tally,
+    }));
+  }, [proposals, tallies]);
+
+  return (
+    <>
+      <table
+        className="table w-full border-separate border-spacing-y-3 -mt-6"
+        aria-label="Group proposals"
+        data-testid="proposals"
+      >
+        <thead>
+          <tr className="text-sm font-medium">
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              ID
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%]" scope="col">
+              Title
+            </th>
+            <th className="bg-transparent px-4 py-2 w-[25%] hidden xl:table-cell" scope="col">
+              Time Left
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell md:hidden hidden xl:table-cell"
+              scope="col"
+            >
+              Type
+            </th>
+            <th
+              className="bg-transparent px-4 py-2 w-[25%] sm:table-cell xxs:hidden hidden 2xl:table-cell"
+              scope="col"
+            >
+              Status
+            </th>
+          </tr>
+        </thead>
+        <tbody className="space-y-4">
+          {proposalTallies.map(({ proposal, tally }) => (
+            <ProposalRow
+              key={`proposal-${proposal.id}`}
+              group={group}
+              proposal={proposal}
+              tally={tally}
+            />
+          ))}
+        </tbody>
+      </table>
+    </>
+  );
+};
+
+const ProposalRow = ({
+  group,
+  proposal,
+  tally,
+}: {
+  group?: ExtendedGroupType;
+  proposal: ProposalSDKType;
+  tally?: QueryTallyResultResponseSDKType;
+}) => {
+  const [showVoteModal, setShowVoteModal] = useState(false);
+  const router = useRouter();
+
+  useEffect(() => {
+    const { proposalId } = router.query;
+    if (proposalId === proposal.id.toString()) {
+      setShowVoteModal(true);
+    }
+  }, [router.query, proposal]);
+
+  const policyAddress = group?.policies[0]?.address ?? '';
+  const policyThreshold =
+    (group?.policies[0]?.decision_policy as ThresholdDecisionPolicySDKType)?.threshold ?? '0';
+
+  const endTime = new Date(proposal?.voting_period_end);
+  const now = new Date();
+  const msPerMinute = 1000 * 60;
+  const msPerHour = msPerMinute * 60;
+  const msPerDay = msPerHour * 24;
+
+  const diff = endTime.getTime() - now.getTime();
+
+  let timeLeft: string;
+
+  if (diff <= 0) {
+    timeLeft = 'none';
+  } else if (diff >= msPerDay) {
+    const days = Math.floor(diff / msPerDay);
+    timeLeft = `${days} day${days === 1 ? '' : 's'}`;
+  } else if (diff >= msPerHour) {
+    const hours = Math.floor(diff / msPerHour);
+    timeLeft = `${hours} hour${hours === 1 ? '' : 's'}`;
+  } else if (diff >= msPerMinute) {
+    const minutes = Math.floor(diff / msPerMinute);
+    timeLeft = `${minutes} minute${minutes === 1 ? '' : 's'}`;
+  } else {
+    timeLeft = 'less than a minute';
+  }
path: components/groups/components/Proposals.tsx
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>❓ Verification inconclusive</summary>

**Add test coverage for time calculation logic**

The time formatting logic appropriately handles different time formats (days, hours, minutes) but lacks test coverage. This UI-critical functionality should be tested.

Consider adding unit tests that verify all time calculation scenarios:
- No time left (`diff <= 0`)
- Multiple days remaining
- Multiple hours remaining
- Multiple minutes remaining
- Less than a minute remaining

---

🏁 Script executed:

```shell
#!/bin/bash
# Check if there are any tests covering the time calculation logic
rg -A 3 "timeLeft" --glob="**/*.test.{ts,tsx}" components/groups/
```

Length of output: 65

---



</details>

**Action: Add Unit Tests for Time Calculation Logic**

The time calculation logic in `components/groups/components/Proposals.tsx` is critical for the UI but currently lacks dedicated test coverage. Please add unit tests that verify the following scenarios:

- **No time left:** When the time difference is zero or negative, the output should be `'none'`.
- **Multiple days remaining:** When the time difference is greater than or equal to one day, the output should correctly pluralize days.
- **Multiple hours remaining:** When the time difference is less than a day but at least one hour, the output should correctly reflect hours.
- **Multiple minutes remaining:** When the time difference is less than an hour but at least one minute, the output should correctly reflect minutes.
- **Less than a minute remaining:** When the time difference is very small, the output should be `'less than a minute'`.

It appears that no tests matching the time calculation logic were found in the current codebase. If tests exist elsewhere, please verify that they cover all these scenarios. Otherwise, adding new tests is required.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-03-18T17:29:34Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-03-13T18:22:28Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough
This pull request introduces a series of UI and refactoring changes primarily targeting the groups and proposals features. A new property is added to disable a button during signing. A new component for displaying group proposals is introduced along with significant refactoring of tab management and proposal rendering. Other modifications include adjustments to import paths, export signatures, and minor changes to modal and token balance logic. Wallet connection handling is also refactored using a dedicated component.

## Changes

| Files | Change Summary |
|-------|----------------|
| `components/factory/forms/ConfirmationForm.tsx` | Added a `disabled` attribute to the button based on the `isSigning` state. |
| `components/groups/components/Proposals.tsx`<br>`components/groups/components/groupControls.tsx` | Introduced a new `GroupProposals` component to handle proposal listing and modal display, and refactored tab management by replacing the `policyAddress` prop with a `group` prop. |
| `components/groups/components/myGroups.tsx`<br>`components/groups/components/__tests__/myGroups.test.tsx` | Updated `YourGroups` from a default to a named export, adjusted import paths, added a new `LoadingGroupRow`, and updated `GroupRow` to use `TokenBalance`. |
| `components/groups/modals/voteDetailsModal.tsx` | Removed the `proposals` property from the `VoteDetailsModalProps` interface. |
| `components/react/TokenBalance.tsx` | Changed the `token` prop type to `Partial<CombinedBalanceInfo>` and added a fallback for `denom`. |
| `components/groups/components/index.tsx` | Added an export statement (`export * from './Proposals';`) to broaden the module’s public API. |
| `pages/groups/index.tsx` | Refactored wallet connection logic by integrating the new `IfWalletConnected` component. |
| `tests/mock.ts` | Modified `mockRouter` to accept an optional `routerConfig` parameter for enhanced flexibility. |

## Possibly related PRs
- liftedinit/manifest-app#338: The changes in the main PR, which involve adding a `disabled` attribute to a button based on the `isSigning` state, are related to the retrieved PR that also modifies modal components to prevent user interaction during signing processes.

## Suggested labels
`enhancement`

## Suggested reviewers
- fmorency

## Poem
> I'm a rabbit hopping through the code so bright,  
> With new tabs and proposals taking flight.  
> The button sleeps when signing's in play,  
> And modals bloom in a magical way.  
> Code paths align, and bugs take their leave—  
> A merry hop into changes we believe! 🐇✨

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡🧪 Multi-step agentic review comment chat (experimental)</summary>
> 
> - We're introducing multi-step agentic chat in review comments. This experimental feature enhances review discussions with the CodeRabbit agentic chat by enabling advanced interactions, including the ability to create pull requests directly from comments.
> 						- To enable this feature, set early_access to true under in the settings.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=liftedinit/manifest-app&utm_content=366):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codecov[bot]
created_at: 2025-03-13T18:44:58Z
body: ## [Codecov](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit) Report
Attention: Patch coverage is `81.31868%` with `68 lines` in your changes missing coverage. Please review.
> Project coverage is 55.72%. Comparing base [(`88d95bf`)](https://app.codecov.io/gh/liftedinit/manifest-app/commit/88d95bf591a611419e5cd0179af31419e6a42ea6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit) to head [(`8728afd`)](https://app.codecov.io/gh/liftedinit/manifest-app/commit/8728afd584716d89866b4ef55ff6b70f4f7c630e?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit).
> Report is 2 commits behind head on main.

| [Files with missing lines](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit) | Patch % | Lines |
|---|---|---|
| [components/groups/components/Proposals.tsx](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?src=pr&el=tree&filepath=components%2Fgroups%2Fcomponents%2FProposals.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit#diff-Y29tcG9uZW50cy9ncm91cHMvY29tcG9uZW50cy9Qcm9wb3NhbHMudHN4) | 80.21% | [36 Missing :warning: ](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit) |
| [components/groups/components/groupControls.tsx](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?src=pr&el=tree&filepath=components%2Fgroups%2Fcomponents%2FgroupControls.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit#diff-Y29tcG9uZW50cy9ncm91cHMvY29tcG9uZW50cy9ncm91cENvbnRyb2xzLnRzeA==) | 75.00% | [29 Missing :warning: ](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit) |
| [components/groups/components/myGroups.tsx](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?src=pr&el=tree&filepath=components%2Fgroups%2Fcomponents%2FmyGroups.tsx&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit#diff-Y29tcG9uZW50cy9ncm91cHMvY29tcG9uZW50cy9teUdyb3Vwcy50c3g=) | 94.54% | [3 Missing :warning: ](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #366      +/-   ##
==========================================
+ Coverage   55.35%   55.72%   +0.37%     
==========================================
  Files         228      229       +1     
  Lines       16290    16246      -44     
==========================================
+ Hits         9017     9053      +36     
+ Misses       7273     7193      -80     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/liftedinit/manifest-app/pull/366?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=liftedinit).

<details><summary>🚀 New features to boost your workflow: </summary>

- ❄ [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- 📦 [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: hansl
created_at: 2025-03-14T15:50:08Z
body: @fmorency Unrelated to this PR, it happens on master as well. Let me investigate.
author_association: CONTRIBUTOR