From a0d05c1f3ded3bcb0c871b9506e67834dce1193b Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 10:02:07 +0200 Subject: [PATCH 01/31] Initial AI version Signed-off-by: Milos Dzepina --- src/modules/components/proposal/index.ts | 1 + .../index.ts | 1 + .../proposalActionSimulationStructure.tsx | 153 ++++++++++++++++++ 3 files changed, 155 insertions(+) create mode 100644 src/modules/components/proposal/proposalActionSimulationStructure/index.ts create mode 100644 src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx diff --git a/src/modules/components/proposal/index.ts b/src/modules/components/proposal/index.ts index 5dd4bb84c..e1c4014f9 100644 --- a/src/modules/components/proposal/index.ts +++ b/src/modules/components/proposal/index.ts @@ -1,3 +1,4 @@ +export * from './proposalActionSimulationStructure'; export * from './proposalActions'; export * from './proposalDataListItem'; export * from './proposalUtils'; diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/index.ts b/src/modules/components/proposal/proposalActionSimulationStructure/index.ts new file mode 100644 index 000000000..6f2746cc5 --- /dev/null +++ b/src/modules/components/proposal/proposalActionSimulationStructure/index.ts @@ -0,0 +1 @@ +export { ProposalActionSimulationStructure } from './proposalActionSimulationStructure'; diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx new file mode 100644 index 000000000..076ba03ec --- /dev/null +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -0,0 +1,153 @@ +import { DateTime } from 'luxon'; +import { Button, DefinitionList, Icon, IconType } from '../../../../core'; +import { formatterUtils } from '../../../../core/utils/formatterUtils'; + +export interface IProposalActionSimulationStructureProps { + /** + * Total number of actions in the proposal + */ + totalActions: number; + /** + * Last simulation date + */ + lastSimulation?: DateTime | string | number; + /** + * Simulation status and state + */ + executionStatus: { + label: string; + isLoading?: boolean; + isExecutable?: boolean; + }; + /** + * Callback when simulate again button is clicked + */ + onSimulateAgain?: () => void; + /** + * Callback when view on tenderly button is clicked + */ + onViewOnTenderly?: () => void; + /** + * Whether the simulate again button is in loading state + */ + isSimulating?: boolean; + /** + * URL for tenderly simulation + */ + tenderlyUrl?: string; +} + +export const ProposalActionSimulationStructure: React.FC = (props) => { + const { + totalActions, + lastSimulation, + executionStatus, + onSimulateAgain, + onViewOnTenderly, + isSimulating = false, + tenderlyUrl, + } = props; + + const handleSimulateAgain = () => { + onSimulateAgain?.(); + }; + + const handleViewOnTenderly = () => { + if (tenderlyUrl) { + window.open(tenderlyUrl, '_blank'); + } else { + onViewOnTenderly?.(); + } + }; + + const formatSimulationDate = (date?: DateTime | string | number) => { + if (!date) { + return null; + } + + const dateTime = typeof date === 'string' + ? DateTime.fromISO(date) + : typeof date === 'number' + ? DateTime.fromMillis(date) + : date; + + const now = DateTime.now(); + const diffInHours = now.diff(dateTime, 'hours').hours; + const diffInDays = now.diff(dateTime, 'days').days; + + if (diffInHours < 1) { + return 'Now'; + } else if (diffInDays < 1) { + const hours = Math.floor(diffInHours); + return `${hours.toString()} hour${hours > 1 ? 's' : ''} ago`; + } else if (diffInDays < 7) { + const days = Math.floor(diffInDays); + return `${days.toString()} day${days > 1 ? 's' : ''} ago`; + } else { + return formatterUtils.formatDate(dateTime); + } + }; + + return ( +
+ + + {`${totalActions.toString()} action${totalActions !== 1 ? 's' : ''}`} + + + + {formatSimulationDate(lastSimulation) ?? 'No simulation yet'} + + + +
+
+ {executionStatus.isLoading ? ( +
+
+
+ ) : ( +
+ +
+ )} + + {executionStatus.label} + +
+
+ Label + +
+
+ + + +
+ + + +
+
+ ); +}; From 8e7deb45329956cac44ee77d1bc7015b3cf86d1f Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 10:02:25 +0200 Subject: [PATCH 02/31] Initial AI version Signed-off-by: Milos Dzepina --- ...posalActionSimulationStructure.stories.tsx | 125 ++++++++++++++ ...proposalActionSimulationStructure.test.tsx | 161 ++++++++++++++++++ 2 files changed, 286 insertions(+) create mode 100644 src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx create mode 100644 src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx new file mode 100644 index 000000000..e9f229951 --- /dev/null +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx @@ -0,0 +1,125 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { DateTime } from 'luxon'; +import { ProposalActionSimulationStructure } from './proposalActionSimulationStructure'; + +const meta: Meta = { + title: 'Modules/Components/Proposal/ProposalActionSimulationStructure', + component: ProposalActionSimulationStructure, + parameters: { + design: { + type: 'figma', + url: 'https://www.figma.com/design/ISSDryshtEpB7SUSdNqAcw/Governance-UI-Kit?node-id=30069-34747&t=tQiF5klPD9cjUit6-4', + }, + }, + args: { + onSimulateAgain: () => { + // Mock action + }, + onViewOnTenderly: () => { + // Mock action + }, + }, +}; + +type Story = StoryObj; + +/** + * Success state showing a completed simulation with executable actions + */ +export const Success: Story = { + args: { + totalActions: 3, + lastSimulation: DateTime.now().minus({ hours: 2 }), + executionStatus: { + label: 'Likely to succeed', + isExecutable: true, + }, + }, +}; + +/** + * Loading state while simulation is running + */ +export const Loading: Story = { + args: { + totalActions: 5, + lastSimulation: DateTime.now().minus({ days: 1 }), + executionStatus: { + label: 'Simulating...', + isLoading: true, + }, + isSimulating: true, + }, +}; + +/** + * Failed simulation with non-executable actions + */ +export const Failed: Story = { + args: { + totalActions: 2, + lastSimulation: DateTime.now().minus({ hours: 6 }), + executionStatus: { + label: 'Likely to fail', + isExecutable: false, + }, + }, +}; + +/** + * No simulation has been run yet + */ +export const NoSimulation: Story = { + args: { + totalActions: 1, + executionStatus: { + label: 'Not simulated', + isExecutable: false, + }, + }, +}; + +/** + * With Tenderly URL for external link + */ +export const WithTenderlyUrl: Story = { + args: { + totalActions: 4, + lastSimulation: DateTime.now().minus({ minutes: 30 }), + executionStatus: { + label: 'Likely to succeed', + isExecutable: true, + }, + tenderlyUrl: 'https://dashboard.tenderly.co/simulation/12345', + }, +}; + +/** + * Recent simulation (shows "Now") + */ +export const RecentSimulation: Story = { + args: { + totalActions: 7, + lastSimulation: DateTime.now().minus({ minutes: 5 }), + executionStatus: { + label: 'Likely to succeed', + isExecutable: true, + }, + }, +}; + +/** + * Old simulation (shows formatted date) + */ +export const OldSimulation: Story = { + args: { + totalActions: 2, + lastSimulation: DateTime.now().minus({ weeks: 2 }), + executionStatus: { + label: 'Likely to succeed', + isExecutable: true, + }, + }, +}; + +export default meta; diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx new file mode 100644 index 000000000..a06ba9c0a --- /dev/null +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx @@ -0,0 +1,161 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { DateTime } from 'luxon'; +import { GukCoreProvider } from '../../../../core'; +import { ProposalActionSimulationStructure } from './proposalActionSimulationStructure'; +import type { IProposalActionSimulationStructureProps } from './proposalActionSimulationStructure'; + +describe(' component', () => { + const createTestComponent = (props?: Partial) => { + const completeProps: IProposalActionSimulationStructureProps = { + totalActions: 3, + executionStatus: { + label: 'Likely to succeed', + isExecutable: true, + }, + ...props, + }; + + return ( + + + + ); + }; + + it('renders the total actions count', () => { + render(createTestComponent({ totalActions: 5 })); + expect(screen.getByText('5 actions')).toBeInTheDocument(); + }); + + it('renders singular action text for one action', () => { + render(createTestComponent({ totalActions: 1 })); + expect(screen.getByText('1 action')).toBeInTheDocument(); + }); + + it('renders "No simulation yet" when no last simulation is provided', () => { + render(createTestComponent()); + expect(screen.getByText('No simulation yet')).toBeInTheDocument(); + }); + + it('renders relative time for recent simulation', () => { + const recentTime = DateTime.now().minus({ minutes: 30 }); + render(createTestComponent({ lastSimulation: recentTime })); + expect(screen.getByText('Now')).toBeInTheDocument(); + }); + + it('renders "Now" for very recent simulation', () => { + const veryRecentTime = DateTime.now().minus({ minutes: 15 }); + render(createTestComponent({ lastSimulation: veryRecentTime })); + expect(screen.getByText('Now')).toBeInTheDocument(); + }); + + it('renders hours ago for simulation within a day', () => { + const hoursAgo = DateTime.now().minus({ hours: 3 }); + render(createTestComponent({ lastSimulation: hoursAgo })); + expect(screen.getByText('3 hours ago')).toBeInTheDocument(); + }); + + it('renders days ago for simulation within a week', () => { + const daysAgo = DateTime.now().minus({ days: 2 }); + render(createTestComponent({ lastSimulation: daysAgo })); + expect(screen.getByText('2 days ago')).toBeInTheDocument(); + }); + + it('renders the execution status label', () => { + render(createTestComponent({ + executionStatus: { label: 'Custom status', isExecutable: true } + })); + expect(screen.getByText('Custom status')).toBeInTheDocument(); + }); + + it('renders loading state when execution status is loading', () => { + render(createTestComponent({ + executionStatus: { label: 'Simulating...', isLoading: true } + })); + + // Check that the loading text is shown + expect(screen.getByText('Simulating...')).toBeInTheDocument(); + + // The success icon should not be present during loading + expect(screen.queryByTestId('SUCCESS')).not.toBeInTheDocument(); + }); + + it('renders success icon when executable', () => { + render(createTestComponent({ + executionStatus: { label: 'Executable', isExecutable: true } + })); + expect(screen.getByTestId('SUCCESS')).toBeInTheDocument(); + }); + + it('calls onSimulateAgain when simulate again button is clicked', async () => { + const user = userEvent.setup(); + const onSimulateAgain = jest.fn(); + + render(createTestComponent({ onSimulateAgain })); + + const simulateButton = screen.getByRole('button', { name: /simulate again/i }); + await user.click(simulateButton); + + expect(onSimulateAgain).toHaveBeenCalledTimes(1); + }); + + it('calls onViewOnTenderly when view on tenderly button is clicked', async () => { + const user = userEvent.setup(); + const onViewOnTenderly = jest.fn(); + + render(createTestComponent({ onViewOnTenderly })); + + const tenderlyButton = screen.getByRole('button', { name: /view on tenderly/i }); + await user.click(tenderlyButton); + + expect(onViewOnTenderly).toHaveBeenCalledTimes(1); + }); + + it('opens tenderly URL in new window when tenderlyUrl is provided', async () => { + const user = userEvent.setup(); + const mockOpen = jest.fn(); + const originalOpen = window.open; + window.open = mockOpen; + + const tenderlyUrl = 'https://dashboard.tenderly.co/simulation/12345'; + render(createTestComponent({ tenderlyUrl })); + + const tenderlyButton = screen.getByRole('button', { name: /view on tenderly/i }); + await user.click(tenderlyButton); + + expect(mockOpen).toHaveBeenCalledWith(tenderlyUrl, '_blank'); + + window.open = originalOpen; + }); + + it('shows loading state on simulate again button when isSimulating is true', () => { + render(createTestComponent({ isSimulating: true })); + + const simulateButton = screen.getByRole('button', { name: /simulate again/i }); + expect(simulateButton).toHaveAttribute('aria-disabled', 'true'); + }); + + it('renders all definition list items', () => { + render(createTestComponent()); + + expect(screen.getByText('Total actions')).toBeInTheDocument(); + expect(screen.getByText('Last simulation')).toBeInTheDocument(); + expect(screen.getByText('Executable')).toBeInTheDocument(); + }); + + it('handles string ISO date input', () => { + const isoDate = '2024-01-15T10:30:00Z'; + render(createTestComponent({ lastSimulation: isoDate })); + + // Should render some form of the date, not "No simulation yet" + expect(screen.queryByText('No simulation yet')).not.toBeInTheDocument(); + }); + + it('handles timestamp number input', () => { + const timestamp = DateTime.now().minus({ hours: 1 }).toMillis(); + render(createTestComponent({ lastSimulation: timestamp })); + + expect(screen.getByText('1 hour ago')).toBeInTheDocument(); + }); +}); From 45c03e2526b322406c7fe56da23cef29d7633fb9 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 10:28:30 +0200 Subject: [PATCH 03/31] Implement MD breakpoint Signed-off-by: Milos Dzepina --- ...posalActionSimulationStructure.stories.tsx | 1 + .../proposalActionSimulationStructure.tsx | 69 +++++++++---------- 2 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx index e9f229951..4a310b042 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx @@ -12,6 +12,7 @@ const meta: Meta = { }, }, args: { + className: 'flex-1', onSimulateAgain: () => { // Mock action }, diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index 076ba03ec..e31ff2bcd 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import { DateTime } from 'luxon'; import { Button, DefinitionList, Icon, IconType } from '../../../../core'; import { formatterUtils } from '../../../../core/utils/formatterUtils'; @@ -35,6 +36,10 @@ export interface IProposalActionSimulationStructureProps { * URL for tenderly simulation */ tenderlyUrl?: string; + /** + * Additional class names applied to the wrapper div. + */ + className?: string; } export const ProposalActionSimulationStructure: React.FC = (props) => { @@ -46,6 +51,7 @@ export const ProposalActionSimulationStructure: React.FC { @@ -64,13 +70,14 @@ export const ProposalActionSimulationStructure: React.FC +
{`${totalActions.toString()} action${totalActions !== 1 ? 's' : ''}`} - + {formatSimulationDate(lastSimulation) ?? 'No simulation yet'} - + -
+
{executionStatus.isLoading ? ( -
-
+
+
) : ( -
- +
)} - - {executionStatus.label} - + {executionStatus.label}
Label @@ -127,24 +134,12 @@ export const ProposalActionSimulationStructure: React.FC -
- - -
From bd6795d1db0fe0ecf76c118a098173787e4a40bc Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 12:34:28 +0200 Subject: [PATCH 04/31] Fix label Signed-off-by: Milos Dzepina --- .../proposalActionSimulationStructure.tsx | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index e31ff2bcd..8d1561aef 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { DateTime } from 'luxon'; -import { Button, DefinitionList, Icon, IconType } from '../../../../core'; +import { AvatarIcon, Button, DefinitionList, IconType } from '../../../../core'; import { formatterUtils } from '../../../../core/utils/formatterUtils'; export interface IProposalActionSimulationStructureProps { @@ -115,20 +115,10 @@ export const ProposalActionSimulationStructure: React.FC ) : (
- +
)} - {executionStatus.label} -
-
- Label - + {executionStatus.label}
From 85296c357b894cfc146f7d81b617a9ac472b597c Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 12:38:41 +0200 Subject: [PATCH 05/31] Wrap in DataListItem Signed-off-by: Milos Dzepina --- .../proposalActionSimulationStructure.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index 8d1561aef..566b786d8 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -1,6 +1,6 @@ import classNames from 'classnames'; import { DateTime } from 'luxon'; -import { AvatarIcon, Button, DefinitionList, IconType } from '../../../../core'; +import { AvatarIcon, Button, DataList, DefinitionList, IconType } from '../../../../core'; import { formatterUtils } from '../../../../core/utils/formatterUtils'; export interface IProposalActionSimulationStructureProps { @@ -96,7 +96,7 @@ export const ProposalActionSimulationStructure: React.FC + {`${totalActions.toString()} action${totalActions !== 1 ? 's' : ''}`} @@ -133,6 +133,6 @@ export const ProposalActionSimulationStructure: React.FC
-
+ ); }; From 7783cb12825dd71664bd1feb1c04dbb84278ae09 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 12:57:51 +0200 Subject: [PATCH 06/31] Add copy Signed-off-by: Milos Dzepina --- src/modules/assets/copy/modulesCopy.ts | 10 ++++++++++ .../proposalActionSimulationStructure.tsx | 17 ++++++++++------- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/modules/assets/copy/modulesCopy.ts b/src/modules/assets/copy/modulesCopy.ts index 9e2fb1098..95b7dff74 100644 --- a/src/modules/assets/copy/modulesCopy.ts +++ b/src/modules/assets/copy/modulesCopy.ts @@ -81,6 +81,16 @@ export const modulesCopy = { totalTokenSupplyTerm: 'Total token supply', totalHoldersTerm: 'Total token holders', }, + proposalActionSimulationStructure: { + totalActionsTerm: 'Total actions', + lastSimulationTerm: 'Last simulation', + never: 'Never', + executableTerm: 'Executable', + simulateAgain: 'Simulate again', + simulate: 'Simulate', + simulating: 'Simulating...', + viewOnTenderly: 'View on Tenderly', + }, proposalDataListItemStatus: { voted: 'Voted', ago: 'ago', diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index 566b786d8..20ac3d27f 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -2,6 +2,7 @@ import classNames from 'classnames'; import { DateTime } from 'luxon'; import { AvatarIcon, Button, DataList, DefinitionList, IconType } from '../../../../core'; import { formatterUtils } from '../../../../core/utils/formatterUtils'; +import { useGukModulesContext } from '../../gukModulesProvider'; export interface IProposalActionSimulationStructureProps { /** @@ -54,6 +55,8 @@ export const ProposalActionSimulationStructure: React.FC { onSimulateAgain?.(); }; @@ -68,7 +71,7 @@ export const ProposalActionSimulationStructure: React.FC { if (!date) { - return null; + return copy.proposalActionSimulationStructure.never; } const dateTime = @@ -98,15 +101,15 @@ export const ProposalActionSimulationStructure: React.FC - + {`${totalActions.toString()} action${totalActions !== 1 ? 's' : ''}`} - - {formatSimulationDate(lastSimulation) ?? 'No simulation yet'} + + {formatSimulationDate(lastSimulation)} - +
{executionStatus.isLoading ? ( @@ -126,11 +129,11 @@ export const ProposalActionSimulationStructure: React.FC
From cf4039971ca9b09ab5a7adc73ef4a04f5be0f1b2 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 13:37:34 +0200 Subject: [PATCH 07/31] Refactor status props Signed-off-by: Milos Dzepina --- src/modules/assets/copy/modulesCopy.ts | 4 +- ...posalActionSimulationStructure.stories.tsx | 38 ++------ ...proposalActionSimulationStructure.test.tsx | 92 ++++++++++-------- .../proposalActionSimulationStructure.tsx | 96 ++++++++++++------- 4 files changed, 123 insertions(+), 107 deletions(-) diff --git a/src/modules/assets/copy/modulesCopy.ts b/src/modules/assets/copy/modulesCopy.ts index 95b7dff74..e23eefbf5 100644 --- a/src/modules/assets/copy/modulesCopy.ts +++ b/src/modules/assets/copy/modulesCopy.ts @@ -88,8 +88,10 @@ export const modulesCopy = { executableTerm: 'Executable', simulateAgain: 'Simulate again', simulate: 'Simulate', - simulating: 'Simulating...', + simulating: 'Simulating', viewOnTenderly: 'View on Tenderly', + likelyToSucceed: 'Likely to succeed', + likelyToFail: 'Likely to fail', }, proposalDataListItemStatus: { voted: 'Voted', diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx index 4a310b042..32cc75c01 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx @@ -16,9 +16,6 @@ const meta: Meta = { onSimulateAgain: () => { // Mock action }, - onViewOnTenderly: () => { - // Mock action - }, }, }; @@ -31,10 +28,7 @@ export const Success: Story = { args: { totalActions: 3, lastSimulation: DateTime.now().minus({ hours: 2 }), - executionStatus: { - label: 'Likely to succeed', - isExecutable: true, - }, + status: 'success', }, }; @@ -45,10 +39,7 @@ export const Loading: Story = { args: { totalActions: 5, lastSimulation: DateTime.now().minus({ days: 1 }), - executionStatus: { - label: 'Simulating...', - isLoading: true, - }, + status: 'unknown', isSimulating: true, }, }; @@ -60,10 +51,7 @@ export const Failed: Story = { args: { totalActions: 2, lastSimulation: DateTime.now().minus({ hours: 6 }), - executionStatus: { - label: 'Likely to fail', - isExecutable: false, - }, + status: 'failure', }, }; @@ -73,10 +61,7 @@ export const Failed: Story = { export const NoSimulation: Story = { args: { totalActions: 1, - executionStatus: { - label: 'Not simulated', - isExecutable: false, - }, + status: 'unknown', }, }; @@ -87,10 +72,7 @@ export const WithTenderlyUrl: Story = { args: { totalActions: 4, lastSimulation: DateTime.now().minus({ minutes: 30 }), - executionStatus: { - label: 'Likely to succeed', - isExecutable: true, - }, + status: 'success', tenderlyUrl: 'https://dashboard.tenderly.co/simulation/12345', }, }; @@ -102,10 +84,7 @@ export const RecentSimulation: Story = { args: { totalActions: 7, lastSimulation: DateTime.now().minus({ minutes: 5 }), - executionStatus: { - label: 'Likely to succeed', - isExecutable: true, - }, + status: 'success', }, }; @@ -116,10 +95,7 @@ export const OldSimulation: Story = { args: { totalActions: 2, lastSimulation: DateTime.now().minus({ weeks: 2 }), - executionStatus: { - label: 'Likely to succeed', - isExecutable: true, - }, + status: 'success', }, }; diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx index a06ba9c0a..b45818794 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx @@ -2,17 +2,14 @@ import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { DateTime } from 'luxon'; import { GukCoreProvider } from '../../../../core'; -import { ProposalActionSimulationStructure } from './proposalActionSimulationStructure'; import type { IProposalActionSimulationStructureProps } from './proposalActionSimulationStructure'; +import { ProposalActionSimulationStructure } from './proposalActionSimulationStructure'; describe(' component', () => { const createTestComponent = (props?: Partial) => { const completeProps: IProposalActionSimulationStructureProps = { totalActions: 3, - executionStatus: { - label: 'Likely to succeed', - isExecutable: true, - }, + status: 'success', ...props, }; @@ -63,53 +60,68 @@ describe(' component', () => { }); it('renders the execution status label', () => { - render(createTestComponent({ - executionStatus: { label: 'Custom status', isExecutable: true } - })); - expect(screen.getByText('Custom status')).toBeInTheDocument(); + render( + createTestComponent({ + status: 'success', + }), + ); + expect(screen.getByText('Likely to succeed')).toBeInTheDocument(); }); it('renders loading state when execution status is loading', () => { - render(createTestComponent({ - executionStatus: { label: 'Simulating...', isLoading: true } - })); - + render( + createTestComponent({ + status: 'unknown', + isSimulating: true, + }), + ); + // Check that the loading text is shown expect(screen.getByText('Simulating...')).toBeInTheDocument(); - + // The success icon should not be present during loading expect(screen.queryByTestId('SUCCESS')).not.toBeInTheDocument(); }); it('renders success icon when executable', () => { - render(createTestComponent({ - executionStatus: { label: 'Executable', isExecutable: true } - })); + render( + createTestComponent({ + status: 'success', + }), + ); expect(screen.getByTestId('SUCCESS')).toBeInTheDocument(); }); + it('renders critical icon when failed', () => { + render( + createTestComponent({ + status: 'failure', + }), + ); + expect(screen.getByTestId('CRITICAL')).toBeInTheDocument(); + expect(screen.getByText('Likely to fail')).toBeInTheDocument(); + }); + + it('renders info icon when unknown', () => { + render( + createTestComponent({ + status: 'unknown', + }), + ); + expect(screen.getByTestId('INFO')).toBeInTheDocument(); + expect(screen.getByText('Not simulated')).toBeInTheDocument(); + }); + it('calls onSimulateAgain when simulate again button is clicked', async () => { const user = userEvent.setup(); const onSimulateAgain = jest.fn(); - + render(createTestComponent({ onSimulateAgain })); - + const simulateButton = screen.getByRole('button', { name: /simulate again/i }); await user.click(simulateButton); - - expect(onSimulateAgain).toHaveBeenCalledTimes(1); - }); - it('calls onViewOnTenderly when view on tenderly button is clicked', async () => { - const user = userEvent.setup(); - const onViewOnTenderly = jest.fn(); - - render(createTestComponent({ onViewOnTenderly })); - - const tenderlyButton = screen.getByRole('button', { name: /view on tenderly/i }); - await user.click(tenderlyButton); - - expect(onViewOnTenderly).toHaveBeenCalledTimes(1); + expect(onSimulateAgain).toHaveBeenCalledTimes(1); }); it('opens tenderly URL in new window when tenderlyUrl is provided', async () => { @@ -117,28 +129,28 @@ describe(' component', () => { const mockOpen = jest.fn(); const originalOpen = window.open; window.open = mockOpen; - + const tenderlyUrl = 'https://dashboard.tenderly.co/simulation/12345'; render(createTestComponent({ tenderlyUrl })); - + const tenderlyButton = screen.getByRole('button', { name: /view on tenderly/i }); await user.click(tenderlyButton); - + expect(mockOpen).toHaveBeenCalledWith(tenderlyUrl, '_blank'); - + window.open = originalOpen; }); it('shows loading state on simulate again button when isSimulating is true', () => { render(createTestComponent({ isSimulating: true })); - + const simulateButton = screen.getByRole('button', { name: /simulate again/i }); expect(simulateButton).toHaveAttribute('aria-disabled', 'true'); }); it('renders all definition list items', () => { render(createTestComponent()); - + expect(screen.getByText('Total actions')).toBeInTheDocument(); expect(screen.getByText('Last simulation')).toBeInTheDocument(); expect(screen.getByText('Executable')).toBeInTheDocument(); @@ -147,7 +159,7 @@ describe(' component', () => { it('handles string ISO date input', () => { const isoDate = '2024-01-15T10:30:00Z'; render(createTestComponent({ lastSimulation: isoDate })); - + // Should render some form of the date, not "No simulation yet" expect(screen.queryByText('No simulation yet')).not.toBeInTheDocument(); }); @@ -155,7 +167,7 @@ describe(' component', () => { it('handles timestamp number input', () => { const timestamp = DateTime.now().minus({ hours: 1 }).toMillis(); render(createTestComponent({ lastSimulation: timestamp })); - + expect(screen.getByText('1 hour ago')).toBeInTheDocument(); }); }); diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index 20ac3d27f..69b56f6bb 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import { DateTime } from 'luxon'; -import { AvatarIcon, Button, DataList, DefinitionList, IconType } from '../../../../core'; -import { formatterUtils } from '../../../../core/utils/formatterUtils'; +import { AvatarIcon, Button, DataList, DefinitionList, formatterUtils, IconType, Spinner } from '../../../../core'; import { useGukModulesContext } from '../../gukModulesProvider'; export interface IProposalActionSimulationStructureProps { @@ -14,25 +13,17 @@ export interface IProposalActionSimulationStructureProps { */ lastSimulation?: DateTime | string | number; /** - * Simulation status and state + * Whether simulation is currently running */ - executionStatus: { - label: string; - isLoading?: boolean; - isExecutable?: boolean; - }; - /** - * Callback when simulate again button is clicked - */ - onSimulateAgain?: () => void; + isSimulating?: boolean; /** - * Callback when view on tenderly button is clicked + * Simulation status result */ - onViewOnTenderly?: () => void; + status: 'success' | 'failure' | 'unknown'; /** - * Whether the simulate again button is in loading state + * Callback when simulate again button is clicked */ - isSimulating?: boolean; + onSimulateAgain?: () => void; /** * URL for tenderly simulation */ @@ -47,26 +38,47 @@ export const ProposalActionSimulationStructure: React.FC { - onSimulateAgain?.(); + const getStatusConfig = () => { + switch (status) { + case 'success': + return { + icon: IconType.CHECKMARK, + label: simulationCopy.likelyToSucceed, + textColor: 'text-success-800', + variant: 'success' as const, + }; + case 'failure': + return { + icon: IconType.CRITICAL, + label: simulationCopy.likelyToFail, + textColor: 'text-critical-800', + variant: 'critical' as const, + }; + case 'unknown': + default: + return { + icon: IconType.INFO, + label: 'Not simulated', + textColor: 'text-neutral-500', + variant: 'neutral' as const, + }; + } }; - const handleViewOnTenderly = () => { - if (tenderlyUrl) { - window.open(tenderlyUrl, '_blank'); - } else { - onViewOnTenderly?.(); - } + const statusConfig = getStatusConfig(); + + const handleSimulateAgain = () => { + onSimulateAgain?.(); }; const formatSimulationDate = (date?: DateTime | string | number) => { @@ -106,22 +118,30 @@ export const ProposalActionSimulationStructure: React.FC - {formatSimulationDate(lastSimulation)} + {isSimulating ? ( + + + {simulationCopy.simulating} + + ) : ( + formatSimulationDate(lastSimulation) + )}
- {executionStatus.isLoading ? ( -
-
-
+ {isSimulating ? ( + + + {simulationCopy.simulating} + ) : (
- +
)} - {executionStatus.label} + {statusConfig.label}
@@ -132,7 +152,13 @@ export const ProposalActionSimulationStructure: React.FC -
From e6aed698bc9cb3240a04b1e5254d4633b75a5588 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 15:05:20 +0200 Subject: [PATCH 08/31] Add error message Signed-off-by: Milos Dzepina --- src/modules/assets/copy/modulesCopy.ts | 1 + ...posalActionSimulationStructure.stories.tsx | 15 ++++-- ...proposalActionSimulationStructure.test.tsx | 2 +- .../proposalActionSimulationStructure.tsx | 49 +++++++++++-------- 4 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/modules/assets/copy/modulesCopy.ts b/src/modules/assets/copy/modulesCopy.ts index e23eefbf5..52032c87f 100644 --- a/src/modules/assets/copy/modulesCopy.ts +++ b/src/modules/assets/copy/modulesCopy.ts @@ -92,6 +92,7 @@ export const modulesCopy = { viewOnTenderly: 'View on Tenderly', likelyToSucceed: 'Likely to succeed', likelyToFail: 'Likely to fail', + unknown: 'Unknown', }, proposalDataListItemStatus: { voted: 'Voted', diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx index 32cc75c01..1e22ae734 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx @@ -13,9 +13,6 @@ const meta: Meta = { }, args: { className: 'flex-1', - onSimulateAgain: () => { - // Mock action - }, }, }; @@ -65,6 +62,18 @@ export const NoSimulation: Story = { }, }; +/** + * With error message displayed + */ +export const WithErrorMessage: Story = { + args: { + totalActions: 2, + lastSimulation: DateTime.now(), + status: 'unknown', + error: 'Simulation failed due to network error. Please try again.', + }, +}; + /** * With Tenderly URL for external link */ diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx index b45818794..4809d489e 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx @@ -116,7 +116,7 @@ describe(' component', () => { const user = userEvent.setup(); const onSimulateAgain = jest.fn(); - render(createTestComponent({ onSimulateAgain })); + render(createTestComponent({ onSimulate: onSimulateAgain })); const simulateButton = screen.getByRole('button', { name: /simulate again/i }); await user.click(simulateButton); diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index 69b56f6bb..73fa2ce27 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -23,7 +23,7 @@ export interface IProposalActionSimulationStructureProps { /** * Callback when simulate again button is clicked */ - onSimulateAgain?: () => void; + onSimulate?: () => void; /** * URL for tenderly simulation */ @@ -32,18 +32,15 @@ export interface IProposalActionSimulationStructureProps { * Additional class names applied to the wrapper div. */ className?: string; + /** + * + * Optional error message to display. + */ + error?: string; } export const ProposalActionSimulationStructure: React.FC = (props) => { - const { - totalActions, - lastSimulation, - isSimulating = false, - status, - onSimulateAgain, - tenderlyUrl, - className, - } = props; + const { totalActions, lastSimulation, isSimulating, status, onSimulate, tenderlyUrl, className, error } = props; const { copy } = useGukModulesContext(); const simulationCopy = copy.proposalActionSimulationStructure; @@ -68,7 +65,7 @@ export const ProposalActionSimulationStructure: React.FC { - onSimulateAgain?.(); - }; - const formatSimulationDate = (date?: DateTime | string | number) => { if (!date) { return copy.proposalActionSimulationStructure.never; @@ -137,19 +130,27 @@ export const ProposalActionSimulationStructure: React.FC ) : ( -
- -
+ <> +
+ +
+ + {statusConfig.label} + + )} - {statusConfig.label}
-
+ {error && ( +

+ + {error} +

+ )} ); }; From 68da41f7d53f4ac608fa3113283d058d7be226bb Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Tue, 26 Aug 2025 15:20:43 +0200 Subject: [PATCH 09/31] Add isSimulatable flag Signed-off-by: Milos Dzepina --- ...posalActionSimulationStructure.stories.tsx | 25 ++++++++++++ ...proposalActionSimulationStructure.test.tsx | 39 +++++++++++++++++++ .../proposalActionSimulationStructure.tsx | 38 +++++++++++++----- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx index 1e22ae734..a53aa6d92 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.stories.tsx @@ -108,4 +108,29 @@ export const OldSimulation: Story = { }, }; +/** + * Not simulatable - only shows Tenderly link + */ +export const NotSimulatable: Story = { + args: { + totalActions: 3, + lastSimulation: DateTime.now().minus({ hours: 2 }), + status: 'success', + isSimulatable: false, + tenderlyUrl: 'https://dashboard.tenderly.co/simulation/12345', + }, +}; + +/** + * Not simulatable without previous simulation + */ +export const NotSimulatableNoSimulation: Story = { + args: { + totalActions: 1, + status: 'unknown', + isSimulatable: false, + tenderlyUrl: 'https://dashboard.tenderly.co/simulation/12345', + }, +}; + export default meta; diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx index 4809d489e..ec54f973a 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.test.tsx @@ -170,4 +170,43 @@ describe(' component', () => { expect(screen.getByText('1 hour ago')).toBeInTheDocument(); }); + + it('shows simulate button by default when isSimulatable is not specified', () => { + render(createTestComponent()); + + expect(screen.getByRole('button', { name: /simulate again/i })).toBeInTheDocument(); + }); + + it('shows simulate button when isSimulatable is true', () => { + render(createTestComponent({ isSimulatable: true })); + + expect(screen.getByRole('button', { name: /simulate again/i })).toBeInTheDocument(); + }); + + it('hides simulate button when isSimulatable is false', () => { + render(createTestComponent({ isSimulatable: false })); + + expect(screen.queryByRole('button', { name: /simulate again/i })).not.toBeInTheDocument(); + }); + + it('always shows tenderly button regardless of isSimulatable value', () => { + render(createTestComponent({ isSimulatable: false, tenderlyUrl: 'https://tenderly.co/test' })); + + expect(screen.queryByRole('button', { name: /simulate again/i })).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: /view on tenderly/i })).toBeInTheDocument(); + }); + + it('positions tenderly button on the right when simulate button is not shown', () => { + render(createTestComponent({ isSimulatable: false })); + + const tenderlyButton = screen.getByRole('button', { name: /view on tenderly/i }); + expect(tenderlyButton).toHaveClass('md:ml-auto'); + }); + + it('does not add ml-auto class to tenderly button when simulate button is shown', () => { + render(createTestComponent({ isSimulatable: true })); + + const tenderlyButton = screen.getByRole('button', { name: /view on tenderly/i }); + expect(tenderlyButton).not.toHaveClass('md:ml-auto'); + }); }); diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx index 73fa2ce27..40d623cea 100644 --- a/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx +++ b/src/modules/components/proposal/proposalActionSimulationStructure/proposalActionSimulationStructure.tsx @@ -16,6 +16,10 @@ export interface IProposalActionSimulationStructureProps { * Whether simulation is currently running */ isSimulating?: boolean; + /** + * Whether the proposal can be simulated (controls if Simulate button is shown) + */ + isSimulatable?: boolean; /** * Simulation status result */ @@ -40,7 +44,17 @@ export interface IProposalActionSimulationStructureProps { } export const ProposalActionSimulationStructure: React.FC = (props) => { - const { totalActions, lastSimulation, isSimulating, status, onSimulate, tenderlyUrl, className, error } = props; + const { + totalActions, + lastSimulation, + isSimulating, + isSimulatable = true, + status, + onSimulate, + tenderlyUrl, + className, + error, + } = props; const { copy } = useGukModulesContext(); const simulationCopy = copy.proposalActionSimulationStructure; @@ -144,14 +158,20 @@ export const ProposalActionSimulationStructure: React.FC -
- +
+ {isSimulatable && ( + + )} )} @@ -140,7 +140,7 @@ export const ProposalActionSimulationStructure: React.FC - {copy.proposalActionSimulationStructure.viewOnTenderly} + {copy.actionSimulation.viewOnTenderly}
{error && ( diff --git a/src/modules/components/action/actionSimulation/index.ts b/src/modules/components/action/actionSimulation/index.ts new file mode 100644 index 000000000..f3fd280ab --- /dev/null +++ b/src/modules/components/action/actionSimulation/index.ts @@ -0,0 +1 @@ +export { ActionSimulation } from './actionSimulation'; diff --git a/src/modules/components/action/index.ts b/src/modules/components/action/index.ts new file mode 100644 index 000000000..cdef3090d --- /dev/null +++ b/src/modules/components/action/index.ts @@ -0,0 +1 @@ +export * from './actionSimulation'; diff --git a/src/modules/components/proposal/index.ts b/src/modules/components/proposal/index.ts index 3df781495..5dd4bb84c 100644 --- a/src/modules/components/proposal/index.ts +++ b/src/modules/components/proposal/index.ts @@ -1,5 +1,4 @@ export * from './proposalActions'; -export * from './proposalActionSimulationStructure'; export * from './proposalDataListItem'; export * from './proposalUtils'; export * from './proposalVoting'; diff --git a/src/modules/components/proposal/proposalActionSimulationStructure/index.ts b/src/modules/components/proposal/proposalActionSimulationStructure/index.ts deleted file mode 100644 index 6f2746cc5..000000000 --- a/src/modules/components/proposal/proposalActionSimulationStructure/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ProposalActionSimulationStructure } from './proposalActionSimulationStructure'; From de79daefb11ee85244b73102f1fe7a55c1161239 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Thu, 28 Aug 2025 14:51:46 +0200 Subject: [PATCH 25/31] Update changeset Signed-off-by: Milos Dzepina --- .changeset/young-flies-think.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/young-flies-think.md b/.changeset/young-flies-think.md index 60e918a1d..0b66556f2 100644 --- a/.changeset/young-flies-think.md +++ b/.changeset/young-flies-think.md @@ -2,4 +2,4 @@ '@aragon/gov-ui-kit': minor --- -Implement `ProposalActionSimulationStructure` component +Implement `ActionSimulation` component From 656198bf69046e903d8ffb673187fec66521afef Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Thu, 28 Aug 2025 14:58:16 +0200 Subject: [PATCH 26/31] Include action module Signed-off-by: Milos Dzepina --- src/modules/components/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/modules/components/index.ts b/src/modules/components/index.ts index aa2ecb3eb..1d2cf0f43 100644 --- a/src/modules/components/index.ts +++ b/src/modules/components/index.ts @@ -1,3 +1,4 @@ +export * from './action'; export * from './address'; export * from './asset'; export * from './dao'; From ed94713008f9d97cf8d5e65c6ac11fdad1bce7ec Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Fri, 29 Aug 2025 14:42:31 +0200 Subject: [PATCH 27/31] p -> div to avoid hydration error Signed-off-by: Milos Dzepina --- .../components/action/actionSimulation/actionSimulation.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/components/action/actionSimulation/actionSimulation.tsx b/src/modules/components/action/actionSimulation/actionSimulation.tsx index 2dafaa286..02c3faf86 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.tsx @@ -144,10 +144,10 @@ export const ActionSimulation: React.FC = (props) => {
{error && ( -

+

{error} -

+
)} ); From 356ad24ab9aee6a7019537b5c6a2da73ef908167 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Fri, 29 Aug 2025 15:38:28 +0200 Subject: [PATCH 28/31] Create IActionSimulationRun interface Signed-off-by: Milos Dzepina --- src/modules/assets/copy/modulesCopy.ts | 1 - .../actionSimulation/actionSimulation.api.tsx | 36 +++++++++++++++++++ .../actionSimulation.stories.tsx | 4 +-- .../actionSimulation.test.tsx | 4 +-- .../actionSimulation/actionSimulation.tsx | 34 ++---------------- .../action/actionSimulation/index.ts | 1 + 6 files changed, 43 insertions(+), 37 deletions(-) create mode 100644 src/modules/components/action/actionSimulation/actionSimulation.api.tsx diff --git a/src/modules/assets/copy/modulesCopy.ts b/src/modules/assets/copy/modulesCopy.ts index e1f686815..86105a53b 100644 --- a/src/modules/assets/copy/modulesCopy.ts +++ b/src/modules/assets/copy/modulesCopy.ts @@ -32,7 +32,6 @@ export const modulesCopy = { likelyToSucceed: 'Likely to succeed', likelyToFail: 'Likely to fail', unknown: 'Unknown', - now: 'Now', }, proposalActionsContainer: { emptyHeader: 'No actions added', diff --git a/src/modules/components/action/actionSimulation/actionSimulation.api.tsx b/src/modules/components/action/actionSimulation/actionSimulation.api.tsx new file mode 100644 index 000000000..5d0044541 --- /dev/null +++ b/src/modules/components/action/actionSimulation/actionSimulation.api.tsx @@ -0,0 +1,36 @@ +export interface IActionSimulationRun { + timestamp: number; + url: string; + status: 'success' | 'failed'; +} + +export interface IActionSimulationProps { + /** + * Total number of actions in the proposal. + */ + totalActions: number; + /** + * Last simulation data including timestamp, URL, and status. + */ + lastSimulation?: IActionSimulationRun; + /** + * Whether simulation is currently running. + */ + isLoading?: boolean; + /** + * Whether the proposal can be simulated. + */ + isEnabled?: boolean; + /** + * Callback when simulate again button is clicked. + */ + onSimulate?: () => void; + /** + * Additional class names applied to the wrapper div. + */ + className?: string; + /** + * Optional error message to display. + */ + error?: string; +} diff --git a/src/modules/components/action/actionSimulation/actionSimulation.stories.tsx b/src/modules/components/action/actionSimulation/actionSimulation.stories.tsx index 350eb92ec..1cf2536af 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.stories.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.stories.tsx @@ -35,13 +35,13 @@ export const Success: Story = { /** * Completed simulation with negative outcome (can't be executed) */ -export const Failure: Story = { +export const Failed: Story = { args: { totalActions: 2, lastSimulation: { timestamp: DateTime.now().minus({ seconds: 10 }).toMillis(), url: 'https://dashboard.tenderly.co/simulation/12345', - status: 'failure', + status: 'failed', }, }, }; diff --git a/src/modules/components/action/actionSimulation/actionSimulation.test.tsx b/src/modules/components/action/actionSimulation/actionSimulation.test.tsx index 9c8315e84..87324fda8 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.test.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.test.tsx @@ -1,8 +1,8 @@ import { render, screen } from '@testing-library/react'; import { DateTime } from 'luxon'; import { GukCoreProvider } from '../../../../core'; -import type { IActionSimulationProps } from './actionSimulation'; import { ActionSimulation } from './actionSimulation'; +import type { IActionSimulationProps } from './actionSimulation.api'; describe(' component', () => { const createTestComponent = (props?: Partial) => { @@ -60,7 +60,7 @@ describe(' component', () => { lastSimulation: { timestamp: DateTime.now().toMillis(), url: 'https://dashboard.tenderly.co/simulation/12345', - status: 'failure', + status: 'failed', }, }), ); diff --git a/src/modules/components/action/actionSimulation/actionSimulation.tsx b/src/modules/components/action/actionSimulation/actionSimulation.tsx index 02c3faf86..4cdf67af3 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.tsx @@ -11,37 +11,7 @@ import { Spinner, } from '../../../../core'; import { useGukModulesContext } from '../../gukModulesProvider'; - -export interface IActionSimulationProps { - /** - * Total number of actions in the proposal. - */ - totalActions: number; - /** - * Last simulation data including timestamp, URL, and status. - */ - lastSimulation?: { timestamp: number; url: string; status: 'success' | 'failure' }; - /** - * Whether simulation is currently running. - */ - isLoading?: boolean; - /** - * Whether the proposal can be simulated. - */ - isEnabled?: boolean; - /** - * Callback when simulate again button is clicked. - */ - onSimulate?: () => void; - /** - * Additional class names applied to the wrapper div. - */ - className?: string; - /** - * Optional error message to display. - */ - error?: string; -} +import type { IActionSimulationProps } from './actionSimulation.api'; export const ActionSimulation: React.FC = (props) => { const { totalActions, lastSimulation, isLoading, isEnabled = true, onSimulate, className, error } = props; @@ -58,7 +28,7 @@ export const ActionSimulation: React.FC = (props) => { textColor: 'text-success-800', variant: 'success' as const, }; - case 'failure': + case 'failed': return { icon: IconType.CRITICAL, label: simulationCopy.likelyToFail, diff --git a/src/modules/components/action/actionSimulation/index.ts b/src/modules/components/action/actionSimulation/index.ts index f3fd280ab..74d62a292 100644 --- a/src/modules/components/action/actionSimulation/index.ts +++ b/src/modules/components/action/actionSimulation/index.ts @@ -1 +1,2 @@ export { ActionSimulation } from './actionSimulation'; +export type { IActionSimulationRun } from './actionSimulation.api'; From cc8fca0f204beb3f61d8d68567b3c48928c198bd Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Fri, 29 Aug 2025 15:48:23 +0200 Subject: [PATCH 29/31] Update tests to use copy Signed-off-by: Milos Dzepina --- .../actionSimulation.test.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/modules/components/action/actionSimulation/actionSimulation.test.tsx b/src/modules/components/action/actionSimulation/actionSimulation.test.tsx index 87324fda8..e32e2403f 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.test.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.test.tsx @@ -1,9 +1,12 @@ import { render, screen } from '@testing-library/react'; import { DateTime } from 'luxon'; import { GukCoreProvider } from '../../../../core'; +import { modulesCopy } from '../../../assets'; import { ActionSimulation } from './actionSimulation'; import type { IActionSimulationProps } from './actionSimulation.api'; +const { actionSimulation: actionSimulationCopy } = modulesCopy; + describe(' component', () => { const createTestComponent = (props?: Partial) => { const completeProps: IActionSimulationProps = { @@ -21,9 +24,9 @@ describe(' component', () => { it('renders all definition list items', () => { render(createTestComponent()); - expect(screen.getByText('Total actions')).toBeInTheDocument(); - expect(screen.getByText('Last simulation')).toBeInTheDocument(); - expect(screen.getByText('Executable')).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.totalActionsTerm)).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.lastSimulationTerm)).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.executableTerm)).toBeInTheDocument(); }); it('renders the total actions count', () => { @@ -38,7 +41,7 @@ describe(' component', () => { it('renders "Never" when no last simulation is provided', () => { render(createTestComponent()); - expect(screen.getByText('Never')).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.never)).toBeInTheDocument(); }); it('renders the execution status label', () => { @@ -51,7 +54,7 @@ describe(' component', () => { }, }), ); - expect(screen.getByText('Likely to succeed')).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.likelyToSucceed)).toBeInTheDocument(); }); it('renders failure status label', () => { @@ -64,12 +67,12 @@ describe(' component', () => { }, }), ); - expect(screen.getByText('Likely to fail')).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.likelyToFail)).toBeInTheDocument(); }); it('renders unknown status when no lastSimulation is provided', () => { render(createTestComponent()); - expect(screen.getByText('Unknown')).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.unknown)).toBeInTheDocument(); }); it('renders loading state when execution status is loading', () => { @@ -79,18 +82,18 @@ describe(' component', () => { }), ); - expect(screen.getAllByText('Simulating')).toHaveLength(3); + expect(screen.getAllByText(actionSimulationCopy.simulating)).toHaveLength(3); }); it('shows simulate button by default when isSimulatable is not specified', () => { render(createTestComponent()); - expect(screen.getByText(/simulate/i)).toBeInTheDocument(); + expect(screen.getByText(actionSimulationCopy.simulate)).toBeInTheDocument(); }); it('hides simulate button when isSimulatable is false', () => { render(createTestComponent({ isEnabled: false })); - expect(screen.queryByText(/simulate/i)).toBeNull(); + expect(screen.queryByText(actionSimulationCopy.simulate)).toBeNull(); }); }); From a78430657affc7e0280aea387fa051b10c86ab55 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Fri, 29 Aug 2025 15:58:05 +0200 Subject: [PATCH 30/31] Address PR commets Signed-off-by: Milos Dzepina --- .../dropdown/dropdownItem/dropdownItem.tsx | 18 +++++++------ .../actionSimulation.test.tsx | 25 +++++-------------- .../actionSimulation/actionSimulation.tsx | 2 +- 3 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/core/components/dropdown/dropdownItem/dropdownItem.tsx b/src/core/components/dropdown/dropdownItem/dropdownItem.tsx index 5d03de97f..48fd0ec75 100644 --- a/src/core/components/dropdown/dropdownItem/dropdownItem.tsx +++ b/src/core/components/dropdown/dropdownItem/dropdownItem.tsx @@ -83,6 +83,15 @@ export const DropdownItem: React.FC = (props) => { }; } + const handleSelect = (event: Event) => { + // For submit buttons, we need to prevent the dropdown from closing immediately to allow the form submission to complete properly + if (renderSubmitButton) { + event.preventDefault(); + } + + props.onSelect?.(event); + }; + const defaultIcon = renderLink ? IconType.LINK_EXTERNAL : selected ? IconType.CHECKMARK : undefined; const processedIcon = icon ?? defaultIcon; @@ -102,14 +111,7 @@ export const DropdownItem: React.FC = (props) => { { 'flex-row-reverse justify-end': iconPosition === 'left' && icon != null }, className, )} - onSelect={(event) => { - // For submit buttons, we need to prevent the dropdown from closing immediately to allow the form submission to complete properly - if (renderSubmitButton) { - event.preventDefault(); - } - - props.onSelect?.(event); - }} + onSelect={handleSelect} {...otherProps} > diff --git a/src/modules/components/action/actionSimulation/actionSimulation.test.tsx b/src/modules/components/action/actionSimulation/actionSimulation.test.tsx index e32e2403f..80c867c33 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.test.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.test.tsx @@ -1,5 +1,4 @@ import { render, screen } from '@testing-library/react'; -import { DateTime } from 'luxon'; import { GukCoreProvider } from '../../../../core'; import { modulesCopy } from '../../../assets'; import { ActionSimulation } from './actionSimulation'; @@ -47,11 +46,7 @@ describe(' component', () => { it('renders the execution status label', () => { render( createTestComponent({ - lastSimulation: { - timestamp: DateTime.now().toMillis(), - url: 'https://dashboard.tenderly.co/simulation/12345', - status: 'success', - }, + lastSimulation: { timestamp: 0, url: '', status: 'success' }, }), ); expect(screen.getByText(actionSimulationCopy.likelyToSucceed)).toBeInTheDocument(); @@ -60,11 +55,7 @@ describe(' component', () => { it('renders failure status label', () => { render( createTestComponent({ - lastSimulation: { - timestamp: DateTime.now().toMillis(), - url: 'https://dashboard.tenderly.co/simulation/12345', - status: 'failed', - }, + lastSimulation: { timestamp: 0, url: '', status: 'failed' }, }), ); expect(screen.getByText(actionSimulationCopy.likelyToFail)).toBeInTheDocument(); @@ -76,24 +67,20 @@ describe(' component', () => { }); it('renders loading state when execution status is loading', () => { - render( - createTestComponent({ - isLoading: true, - }), - ); + render(createTestComponent({ isLoading: true })); expect(screen.getAllByText(actionSimulationCopy.simulating)).toHaveLength(3); }); - it('shows simulate button by default when isSimulatable is not specified', () => { + it('shows simulate button by default when isEnabled is not specified', () => { render(createTestComponent()); expect(screen.getByText(actionSimulationCopy.simulate)).toBeInTheDocument(); }); - it('hides simulate button when isSimulatable is false', () => { + it('hides simulate button when isEnabled is false', () => { render(createTestComponent({ isEnabled: false })); - expect(screen.queryByText(actionSimulationCopy.simulate)).toBeNull(); + expect(screen.queryByText(actionSimulationCopy.simulate)).not.toBeInTheDocument(); }); }); diff --git a/src/modules/components/action/actionSimulation/actionSimulation.tsx b/src/modules/components/action/actionSimulation/actionSimulation.tsx index 4cdf67af3..97d819c97 100644 --- a/src/modules/components/action/actionSimulation/actionSimulation.tsx +++ b/src/modules/components/action/actionSimulation/actionSimulation.tsx @@ -81,7 +81,7 @@ export const ActionSimulation: React.FC = (props) => {
- + {statusConfig.label} From 0b69f28133887a3f64969070941f69aa14d2a606 Mon Sep 17 00:00:00 2001 From: Milos Dzepina Date: Fri, 29 Aug 2025 16:20:56 +0200 Subject: [PATCH 31/31] Update exports Signed-off-by: Milos Dzepina --- src/modules/components/action/actionSimulation/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/components/action/actionSimulation/index.ts b/src/modules/components/action/actionSimulation/index.ts index 74d62a292..731ad9a38 100644 --- a/src/modules/components/action/actionSimulation/index.ts +++ b/src/modules/components/action/actionSimulation/index.ts @@ -1,2 +1,2 @@ export { ActionSimulation } from './actionSimulation'; -export type { IActionSimulationRun } from './actionSimulation.api'; +export type { IActionSimulationProps, IActionSimulationRun } from './actionSimulation.api';