diff --git a/src/custom/SubscriptionTable/SubscriptionTable.test.tsx b/src/custom/SubscriptionTable/SubscriptionTable.test.tsx new file mode 100644 index 000000000..d6ca54fd4 --- /dev/null +++ b/src/custom/SubscriptionTable/SubscriptionTable.test.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { SubscriptionTable, PlanFeature } from './SubscriptionTable'; + +const mockFeatures: PlanFeature[] = [ + { + featureName: 'Cloud Storage', + freePlan: '5GB', + teamPlan: '50GB', + enterprisePlan: 'Unlimited' + }, + { + featureName: 'Priority Support', + freePlan: false, + teamPlan: true, + enterprisePlan: true + } +]; + +describe('SubscriptionTable Component', () => { + it('renders table title and feature rows correctly', () => { + render(); + expect(screen.getByText('Custom Pricing Table')).toBeInTheDocument(); + expect(screen.getByText('Cloud Storage')).toBeInTheDocument(); + expect(screen.getByText('Priority Support')).toBeInTheDocument(); + expect(screen.getByText('5GB')).toBeInTheDocument(); + }); + + it('renders boolean indicators with accessibility labels', () => { + render(); + // Assert actual accessible text labels instead of test IDs + expect(screen.getAllByLabelText('Included').length).toBeGreaterThan(0); + expect(screen.getAllByLabelText('Not included').length).toBeGreaterThan(0); + }); + + it('calls onPlanSelect when action buttons are clicked', () => { + const handlePlanSelect = jest.fn(); + render(); + + fireEvent.click(screen.getByText('Get Started')); + expect(handlePlanSelect).toHaveBeenCalledWith('free'); + + fireEvent.click(screen.getByText('Upgrade')); + expect(handlePlanSelect).toHaveBeenCalledWith('team'); + + fireEvent.click(screen.getByText('Contact Us')); + expect(handlePlanSelect).toHaveBeenCalledWith('enterprise'); + }); + + it('renders custom labels when passed via props', () => { + render( + + ); + expect(screen.getByText('Key Capabilities')).toBeInTheDocument(); + expect(screen.getByText('Starter')).toBeInTheDocument(); + expect(screen.getByText('Join Now')).toBeInTheDocument(); + }); +}); diff --git a/src/custom/SubscriptionTable/SubscriptionTable.tsx b/src/custom/SubscriptionTable/SubscriptionTable.tsx new file mode 100644 index 000000000..eb751a542 --- /dev/null +++ b/src/custom/SubscriptionTable/SubscriptionTable.tsx @@ -0,0 +1,145 @@ +import CheckIcon from '@mui/icons-material/Check'; +import CloseIcon from '@mui/icons-material/Close'; +import { Box, Button, Table, TableBody, TableHead, TableRow, Typography } from '@mui/material'; +import React from 'react'; +import { FeatureHeaderCell, StyledHeaderRow, StyledTableCell, StyledTableContainer } from './style'; + +export interface PlanFeature { + featureName: string; + freePlan: boolean | string; + teamPlan: boolean | string; + enterprisePlan: boolean | string; +} + +export interface SubscriptionTableProps { + title?: string; + features?: PlanFeature[]; + onPlanSelect?: (planType: 'free' | 'team' | 'enterprise') => void; + featuresLabel?: string; + freePlanLabel?: string; + freePlanButtonLabel?: string; + teamPlanLabel?: string; + teamPlanButtonLabel?: string; + enterprisePlanLabel?: string; + enterprisePlanButtonLabel?: string; +} + +export const SubscriptionTable: React.FC = ({ + title = 'Subscription Plans Comparison', + features = [], + onPlanSelect, + featuresLabel = 'Features', + freePlanLabel = 'Free Plan', + freePlanButtonLabel = 'Get Started', + teamPlanLabel = 'Team Plan', + teamPlanButtonLabel = 'Upgrade', + enterprisePlanLabel = 'Enterprise Plan', + enterprisePlanButtonLabel = 'Contact Us' +}) => { + const renderValue = (value: boolean | string) => { + if (typeof value === 'boolean') { + return value ? ( + + ) : ( + + ); + } + return ( + + {value} + + ); + }; + + return ( + + {title && ( + + {title} + + )} + + + + + + + {featuresLabel} + + + + + {freePlanLabel} + + + + + + + + {teamPlanLabel} + + + + + + + + {enterprisePlanLabel} + + + + + + + + + {features.map((row) => ( + + + {row.featureName} + + {renderValue(row.freePlan)} + {renderValue(row.teamPlan)} + {renderValue(row.enterprisePlan)} + + ))} + +
+
+
+ ); +}; + +SubscriptionTable.displayName = 'SubscriptionTable'; + +export default SubscriptionTable; diff --git a/src/custom/SubscriptionTable/index.ts b/src/custom/SubscriptionTable/index.ts new file mode 100644 index 000000000..121092eba --- /dev/null +++ b/src/custom/SubscriptionTable/index.ts @@ -0,0 +1 @@ +export * from './SubscriptionTable'; diff --git a/src/custom/SubscriptionTable/style.tsx b/src/custom/SubscriptionTable/style.tsx new file mode 100644 index 000000000..c22334870 --- /dev/null +++ b/src/custom/SubscriptionTable/style.tsx @@ -0,0 +1,26 @@ +import { Paper, TableCell, TableRow } from '@mui/material'; +import { styled } from '@mui/material/styles'; + +export const StyledTableContainer = styled(Paper)(({ theme }) => ({ + borderRadius: theme.shape.borderRadius * 2, + overflowX: 'auto', + overflowY: 'hidden', + boxShadow: theme.shadows[2], + border: `1px solid ${theme.palette.divider}` +})); + +export const StyledHeaderRow = styled(TableRow)(({ theme }) => ({ + backgroundColor: theme.palette.mode === 'light' ? theme.palette.grey[50] : theme.palette.grey[900] +})); + +export const StyledTableCell = styled(TableCell)(({ theme }) => ({ + fontFamily: theme.typography.fontFamily, + borderColor: theme.palette.divider, + fontWeight: 500 +})); + +export const FeatureHeaderCell = styled(StyledTableCell)(({ theme }) => ({ + fontFamily: theme.typography.fontFamily, + fontWeight: 700, + color: theme.palette.text.primary +})); diff --git a/src/custom/index.ts b/src/custom/index.ts index bfc9cd185..e20c7e12a 100644 --- a/src/custom/index.ts +++ b/src/custom/index.ts @@ -6,5 +6,6 @@ export * from './Markdown'; export * from './Modal'; export * from './RJSFFormWrapper'; export * from './StyledAccordion'; +export * from './SubscriptionTable'; export * from './WidgetPicker'; -export * from './WidgetEmptyState'; +export * from './WidgetEmptyState'; \ No newline at end of file diff --git a/src/custom/index.tsx b/src/custom/index.tsx index bff31634c..6a031ee0f 100644 --- a/src/custom/index.tsx +++ b/src/custom/index.tsx @@ -191,5 +191,6 @@ export * from './permissions'; export * from './ResourceDetailFormatters'; export * from './RJSFFormWrapper'; export * from './ShareModal'; +export * from './SubscriptionTable'; export * from './UserSearchField'; export * from './Workspaces'; diff --git a/src/index.tsx b/src/index.tsx index 24a87e1a1..8a0893380 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -164,3 +164,11 @@ export { type Team as TeamPickerRecord, type TeamSearchFieldProps } from './custom/DashboardWidgets/GettingStartedWidget/TeamSearchField'; + +// Explicit re-export for SubscriptionTable to prevent rollup-plugin-dts from +// dropping the component and its types from the generated dist/index.d.ts bundle. +export { + SubscriptionTable, + type SubscriptionTableProps, + type PlanFeature +} from './custom/SubscriptionTable';