Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions app/src/components/button/DangerButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import AddIcon from '@mui/icons-material/Add';
import { render } from 'test-helpers/test-utils';
import { DangerButton } from './DangerButton';

describe('DangerButton', () => {
it('applies danger defaults', () => {
const { getByRole } = render(<DangerButton>Delete</DangerButton>);

const button = getByRole('button', { name: 'Delete' });

expect(button.className).toContain('MuiButton-contained');
expect(button.className).toContain('MuiButton-containedError');
expect(button.className).toContain('MuiButton-sizeMedium');
});

it('allows overriding defaults', () => {
const { getByRole } = render(
<DangerButton color="primary" size="small">
Delete
</DangerButton>
);

const button = getByRole('button', { name: 'Delete' });

expect(button.className).toContain('MuiButton-containedPrimary');
expect(button.className).toContain('MuiButton-sizeSmall');
});

it('renders end icon', () => {
const { getByTestId } = render(<DangerButton endIcon={<AddIcon data-testid="end-icon" />}>Delete</DangerButton>);

expect(getByTestId('end-icon')).toBeVisible();
});
});
11 changes: 11 additions & 0 deletions app/src/components/button/DangerButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Button, { ButtonProps } from '@mui/material/Button';

/**
* Semantic destructive action button.
*
* @param {ButtonProps} props
* @return {*}
*/
export const DangerButton = (props: ButtonProps) => {
return <Button variant="contained" color="error" size="medium" {...props} />;
};
39 changes: 39 additions & 0 deletions app/src/components/button/PrimaryButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import AddIcon from '@mui/icons-material/Add';
import { render } from 'test-helpers/test-utils';
import { PrimaryButton } from './PrimaryButton';

describe('PrimaryButton', () => {
it('applies primary defaults', () => {
const { getByRole } = render(<PrimaryButton>Create</PrimaryButton>);

const button = getByRole('button', { name: 'Create' });

expect(button.className).toContain('MuiButton-contained');
expect(button.className).toContain('MuiButton-containedPrimary');
expect(button.className).toContain('MuiButton-sizeMedium');
});

it('allows overriding defaults', () => {
const { getByRole } = render(
<PrimaryButton variant="text" size="small">
Create
</PrimaryButton>
);

const button = getByRole('button', { name: 'Create' });

expect(button.className).toContain('MuiButton-text');
expect(button.className).toContain('MuiButton-sizeSmall');
});

it('renders start and end icons', () => {
const { getByTestId } = render(
<PrimaryButton startIcon={<AddIcon data-testid="start-icon" />} endIcon={<AddIcon data-testid="end-icon" />}>
Create
</PrimaryButton>
);

expect(getByTestId('start-icon')).toBeVisible();
expect(getByTestId('end-icon')).toBeVisible();
});
});
11 changes: 11 additions & 0 deletions app/src/components/button/PrimaryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Button, { ButtonProps } from '@mui/material/Button';

/**
* Semantic primary call-to-action button.
*
* @param {ButtonProps} props
* @return {*}
*/
export const PrimaryButton = (props: ButtonProps) => {
return <Button variant="contained" color="primary" size="medium" {...props} />;
};
36 changes: 36 additions & 0 deletions app/src/components/button/SecondaryButton.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import AddIcon from '@mui/icons-material/Add';
import { render } from 'test-helpers/test-utils';
import { SecondaryButton } from './SecondaryButton';

describe('SecondaryButton', () => {
it('applies secondary defaults', () => {
const { getByRole } = render(<SecondaryButton>View</SecondaryButton>);

const button = getByRole('button', { name: 'View' });

expect(button.className).toContain('MuiButton-outlined');
expect(button.className).toContain('MuiButton-outlinedPrimary');
expect(button.className).toContain('MuiButton-sizeMedium');
});

it('allows overriding defaults', () => {
const { getByRole } = render(
<SecondaryButton variant="contained" size="small">
View
</SecondaryButton>
);

const button = getByRole('button', { name: 'View' });

expect(button.className).toContain('MuiButton-contained');
expect(button.className).toContain('MuiButton-sizeSmall');
});

it('renders start icon', () => {
const { getByTestId } = render(
<SecondaryButton startIcon={<AddIcon data-testid="start-icon" />}>View</SecondaryButton>
);

expect(getByTestId('start-icon')).toBeVisible();
});
});
11 changes: 11 additions & 0 deletions app/src/components/button/SecondaryButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import Button, { ButtonProps } from '@mui/material/Button';

/**
* Semantic secondary action button.
*
* @param {ButtonProps} props
* @return {*}
*/
export const SecondaryButton = (props: ButtonProps) => {
return <Button variant="outlined" color="primary" size="medium" {...props} />;
};
176 changes: 176 additions & 0 deletions app/src/components/data-grid/CustomDataGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
import { grey } from '@mui/material/colors';
import { alpha } from '@mui/material/styles';
import { DataGrid, type DataGridProps, type GridValidRowModel } from '@mui/x-data-grid';
import { SkeletonTable } from 'components/loading/SkeletonLoaders';
import React, { useCallback } from 'react';
import StyledDataGridOverlay from './StyledDataGridOverlay';

export type ICustomDataGridProps<R extends GridValidRowModel = GridValidRowModel> = DataGridProps<R> & {
noRowsMessage?: string;
noRowsOverlay?: React.ReactElement;
};

/**
* Standardized DataGrid wrapper that applies shared table styles while preserving full MUI DataGrid API support.
*/
const CustomDataGrid = <R extends GridValidRowModel = GridValidRowModel>(props: ICustomDataGridProps<R>) => {
const { sx, noRowsMessage, noRowsOverlay, slots, ...rest } = props;

const loadingOverlay = () => <SkeletonTable />;

const defaultNoRowsOverlay = useCallback(
() => noRowsOverlay ?? <StyledDataGridOverlay message={noRowsMessage} />,
[noRowsMessage, noRowsOverlay]
);

return (
<DataGrid
{...rest}
disableColumnMenu={true}
disableColumnResize={true}
slots={{
loadingOverlay,
noRowsOverlay: defaultNoRowsOverlay,
...slots
}}
sx={[
(theme) => {
return {
height: '100%',
bgcolor: theme.palette.background.paper,
display: 'flex',
flexDirection: 'column',
borderRadius: 2,
border: 'none !important',

'& *:focus-within': {
outline: 'none !important'
},

'& .MuiDataGrid-columnHeaders': {
bgcolor: theme.palette.background.paper,
minHeight: 56,
height: 56,
maxHeight: 56
},
'& .MuiDataGrid-columnHeader': {
bgcolor: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider} !important`,
minHeight: 56,
height: 56,
maxHeight: 56,
px: 2,
alignItems: 'center'
},
'& .MuiDataGrid-columnHeaderTitleContainer': {
minHeight: 56,
height: 56,
maxHeight: 56,
minWidth: 72
},
'& .MuiDataGrid-columnHeaderTitleContainerContent': {
justifyContent: 'flex-start',
width: '100%',
minWidth: 0
},
'& .MuiDataGrid-columnHeaderTitle': {
textTransform: 'uppercase',
fontWeight: 700,
fontSize: '0.7rem',
color: theme.palette.text.secondary
},
'& .MuiDataGrid-columnSeparator': {
display: 'none'
},
'& .MuiDataGrid-sortIcon, & .MuiDataGrid-iconButtonContainer .MuiSvgIcon-root': {
color: grey[500]
},

'& .MuiDataGrid-cell:not(.MuiDataGrid-cellCheckbox) > *': {
minWidth: 0,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},

'& .MuiDataGrid-row': {
bgcolor: theme.palette.background.paper,
borderBottom: `1px solid ${theme.palette.divider}`,
borderTop: 'none',
borderRight: `1px solid ${theme.palette.divider}`,
cursor: rest.onRowClick ? 'pointer' : 'default'
},
'& .MuiDataGrid-row.Mui-selected': {
bgcolor: `${alpha(theme.palette.primary.main, 0.14)} !important`
},
'& .MuiDataGrid-row.Mui-selected:hover': {
bgcolor: `${alpha(theme.palette.primary.main, 0.18)} !important`
},
'& .MuiDataGrid-row:hover:not(.Mui-selected)': {
bgcolor: theme.palette.action.hover
},

'& .MuiDataGrid-cell': {
bgcolor: 'inherit',
border: 'none',
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
textAlign: 'left',
px: 2,
fontSize: '0.85rem !important',
minHeight: '0 !important',
overflow: 'hidden'
},

'& .MuiDataGrid-cellContent': {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-start',
textAlign: 'left',
width: '100%',
minWidth: 0,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},

'& .MuiDataGrid-cell .MuiTypography-root': {
fontSize: '0.85rem !important',
minWidth: 0,
overflow: 'hidden',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis'
},

'& .MuiDataGrid-cellEmpty': {
padding: '0 !important'
},

'& .MuiDataGrid-footerContainer': {
bgcolor: theme.palette.background.paper,
borderTop: `1px solid ${theme.palette.divider}`
},
'& .MuiDataGrid-virtualScroller': {
bgcolor: theme.palette.background.paper
},
'& .MuiDataGrid-main': {
overflow: 'hidden'
},
'& .MuiDataGrid-row:last-of-type .MuiDataGrid-cell': {
borderBottom: 'none'
},

'&.MuiDataGrid-root--densityCompact .MuiDataGrid-row': {
padding: '4px',
minHeight: 100
}
};
},
...(Array.isArray(sx) ? sx : [sx])
]}
/>
);
};

export default CustomDataGrid;
32 changes: 32 additions & 0 deletions app/src/components/data-grid/StyledDataGridOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';

export interface IStyledDataGridOverlayProps {
message?: string;
}

/**
* Default no-rows overlay for data grids.
*
* @param {IStyledDataGridOverlayProps} props
* @return {*}
*/
const StyledDataGridOverlay: React.FC<IStyledDataGridOverlayProps> = (props) => {
return (
<Box
sx={{
width: '100%',
minHeight: 160,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
px: 2
}}>
<Typography align="center" color="text.secondary">
{props.message || 'No records to display'}
</Typography>
</Box>
);
};

export default StyledDataGridOverlay;
4 changes: 2 additions & 2 deletions app/src/components/dialog/EditDialog.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { fireEvent, waitFor } from '@testing-library/react';
import EditDialog from 'components/dialog/EditDialog';
import CustomTextField from 'components/fields/CustomTextField';
import CustomTextFieldFormik from 'components/fields/CustomTextFieldFormik';
import { useFormikContext } from 'formik';
import { render } from 'test-helpers/test-utils';
import yup from 'utils/YupSchema';
Expand All @@ -20,7 +20,7 @@ const SampleFormikForm = () => {

return (
<form onSubmit={handleSubmit}>
<CustomTextField name="testField" label="Test Field" other={{ multiline: true, required: true, rows: 4 }} />
<CustomTextFieldFormik name="testField" label="Test Field" multiline required rows={4} />
</form>
);
};
Expand Down
7 changes: 1 addition & 6 deletions app/src/components/dialog/EditDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,7 @@ export const EditDialog = <T extends FormikValues>(props: React.PropsWithChildre
props.onSave(values);
}}>
{(formikProps) => (
<Dialog
fullWidth
maxWidth="md"
open={props.open}
aria-labelledby="edit-dialog-title"
aria-describedby="edit-dialog-description">
<Dialog open={props.open} aria-labelledby="edit-dialog-title" aria-describedby="edit-dialog-description">
<DialogTitle id="edit-dialog-title">{props.dialogTitle}</DialogTitle>
<DialogContent>{props.component.element}</DialogContent>
<DialogActions>
Expand Down
Loading
Loading