Skip to content

Commit

Permalink
Update CSV Error Import Dialog Styling (#1456)
Browse files Browse the repository at this point in the history
- update csv import error dialog
- replace failure snackbar & add frontend pagination to csv import errors
  • Loading branch information
mauberti-bc authored Jan 15, 2025
1 parent ef532a3 commit fedb4e9
Show file tree
Hide file tree
Showing 12 changed files with 171 additions and 192 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ describe('TelemetryHeaderConfigs', () => {
const result = cellValidator({ cell: 5555 } as CSVParams);
expect(result).to.deep.equal([
{
error: 'Device not found in the survey deployments',
solution: 'Check the serial number and vendor are correct and the device is deployed in the survey'
error: 'Device not found in deployments',
solution: 'Check that the serial number and vendor match a deployment in the Survey'
}
]);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ export const getTelemetrySerialCellValidator = (
if (!deployment) {
return [
{
error: `Device not found in the survey deployments`,
solution: `Check the serial number and vendor are correct and the device is deployed in the survey`
error: `Device not found in deployments`,
solution: `Check that the serial number and vendor match a deployment in the Survey`
}
];
}
Expand Down
6 changes: 3 additions & 3 deletions api/src/utils/csv-utils/csv-config-validation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ describe('csv-config-validation', () => {
errors: [
{
error: 'A required column is missing',
solution: `Add all required columns to the file.`,
solution: `Add the ALIAS column to the file.`,
header: 'ALIAS',
values: ['ALIAS', 'ALIAS_2'],
cell: null,
Expand Down Expand Up @@ -217,7 +217,7 @@ describe('csv-config-validation', () => {
{
row: 1,
error: 'A required column is missing',
solution: `Add all required columns to the file.`,
solution: `Add the ALIAS column to the file.`,
header: 'ALIAS',
values: ['ALIAS'],
cell: null
Expand Down Expand Up @@ -247,7 +247,7 @@ describe('csv-config-validation', () => {
{
row: 1,
error: 'An unknown column is included in the file',
solution: `Remove extra columns from the file.`,
solution: `Remove the UNKNOWN_HEADER column from the file.`,
header: 'UNKNOWN_HEADER',
cell: null,
values: null
Expand Down
4 changes: 2 additions & 2 deletions api/src/utils/csv-utils/csv-config-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export const validateCSVHeaders = (worksheet: WorkSheet, config: CSVConfig): Req
if (!headerConfig.optional && !worksheetHasStaticHeader) {
csvErrors.push({
error: 'A required column is missing',
solution: `Add all required columns to the file.`,
solution: `Add the ${staticHeader} column to the file.`,
values: [staticHeader, ...config.staticHeadersConfig[staticHeader].aliases],
header: staticHeader,
cell: null,
Expand All @@ -142,7 +142,7 @@ export const validateCSVHeaders = (worksheet: WorkSheet, config: CSVConfig): Req
for (const unknownHeader of configUtils.worksheetDynamicHeaders) {
csvErrors.push({
error: 'An unknown column is included in the file',
solution: `Remove extra columns from the file.`,
solution: `Remove the ${unknownHeader} column from the file.`,
values: null,
header: unknownHeader,
cell: null,
Expand Down
13 changes: 10 additions & 3 deletions app/src/components/alert/AlertBar.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import Alert, { AlertProps } from '@mui/material/Alert';
import AlertTitle from '@mui/material/AlertTitle';
import Typography from '@mui/material/Typography';

interface IAlertBarProps extends AlertProps {
severity: 'error' | 'warning' | 'info' | 'success';
variant: 'filled' | 'outlined' | 'standard';
title: string;
text: string | JSX.Element;
ornament?: JSX.Element;
}

/**
Expand All @@ -15,7 +17,7 @@ interface IAlertBarProps extends AlertProps {
* @returns
*/
const AlertBar = (props: IAlertBarProps) => {
const { severity, variant, title, text, ...alertProps } = props;
const { severity, variant, title, text, ornament, ...alertProps } = props;

const defaultProps = {
severity: 'success',
Expand All @@ -30,8 +32,13 @@ const AlertBar = (props: IAlertBarProps) => {
{...alertProps}
variant={variant}
severity={severity}
sx={{ flex: '1 1 auto', ...alertProps.sx }}>
<AlertTitle>{title}</AlertTitle>
sx={{ flex: '1 1 auto', '& .MuiAlert-message': { flex: '1 1 auto' }, ...alertProps.sx }}>
<AlertTitle sx={{ justifyContent: 'space-between', display: 'flex', alignItems: 'center', flex: '1 1 auto' }}>
{title}
<Typography component="span" variant="body2">
{ornament}
</Typography>
</AlertTitle>
{text}
</Alert>
);
Expand Down
35 changes: 18 additions & 17 deletions app/src/components/csv/CSVDropzoneSection.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { Box } from '@mui/material';
import { Box, Typography } from '@mui/material';
import Button from '@mui/material/Button';
import { CSVErrorsTableContainer } from 'components/csv/CSVErrorsTableContainer';
import HorizontalSplitFormComponent from 'components/fields/HorizontalSplitFormComponent';
import { PropsWithChildren } from 'react';
import { CSVError } from 'utils/csv-utils';
import { CSVErrorsCardStackContainer } from './CSVErrorsCardStackContainer';

interface CSVDropzoneSectionProps {
title: string;
Expand All @@ -21,20 +20,22 @@ interface CSVDropzoneSectionProps {
*/
export const CSVDropzoneSection = (props: PropsWithChildren<CSVDropzoneSectionProps>) => {
return (
<HorizontalSplitFormComponent title={props.title} summary={props.summary}>
<Box sx={{ display: 'flex', flexDirection: 'column' }} gap={2}>
<Box sx={{ display: 'flex', ml: 'auto' }}>
<Button
sx={{ textTransform: 'none', fontWeight: 'regular' }}
variant="outlined"
size="small"
onClick={props.onDownloadTemplate}>
Download Template
</Button>
</Box>
{props.children}
{props.errors.length > 0 ? <CSVErrorsTableContainer errors={props.errors} /> : null}
<Box sx={{ display: 'flex', flexDirection: 'column' }} gap={2}>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Typography variant="h3">{props.title}</Typography>
<Button
sx={{ textTransform: 'none', fontWeight: 'regular' }}
variant="outlined"
size="small"
onClick={props.onDownloadTemplate}>
Download Template
</Button>
</Box>
</HorizontalSplitFormComponent>
<Typography color="textSecondary" variant="body2">
{props.summary}
</Typography>
{props.children}
{props.errors.length > 0 ? <CSVErrorsCardStackContainer errors={props.errors} /> : null}
</Box>
);
};
109 changes: 109 additions & 0 deletions app/src/components/csv/CSVErrorsCardStack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import Icon from '@mdi/react';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import AlertBar from 'components/alert/AlertBar';
import { useMemo, useState } from 'react';
import { CSVError } from 'utils/csv-utils';
import { v4 } from 'uuid';

const MAX_ERRORS_SHOWN = 10;

interface CSVErrorsCardStackProps {
errors: CSVError[];
}

/**
* Returns a stack of CSV errors with information about solutions and pagination
*
* @param {CSVErrorsCardStackProps} props
* @returns {*}
*/
export const CSVErrorsCardStack = (props: CSVErrorsCardStackProps) => {
const [currentPage, setCurrentPage] = useState(0);

const pageCount = Math.ceil(props.errors.length / MAX_ERRORS_SHOWN);

const rows: (CSVError & { id: string })[] = useMemo(() => {
return props.errors.slice(currentPage * MAX_ERRORS_SHOWN, (currentPage + 1) * MAX_ERRORS_SHOWN).map((error) => {
return {
id: v4(),
...error
};
});
}, [props.errors, currentPage]);

const handleNextPage = () => {
if ((currentPage + 1) * MAX_ERRORS_SHOWN < props.errors.length) {
setCurrentPage(currentPage + 1);
}
};

const handlePreviousPage = () => {
if (currentPage > 0) {
setCurrentPage(currentPage - 1);
}
};

return (
<Stack gap={1}>
{rows.map((error) => {
return (
<AlertBar
key={error.id}
severity="error"
variant="standard"
title={error.error}
text={
<Stack gap={1}>
<Typography variant="body2">{error.solution}</Typography>
<Stack gap={3} flexDirection="row">
<Stack>
<Typography variant="body2" fontWeight={700}>
Row
</Typography>
<Typography variant="body2">{error.row ?? 'N/A'}</Typography>
</Stack>
<Stack>
<Typography variant="body2" fontWeight={700}>
Column
</Typography>
<Typography variant="body2">{error.header ?? 'N/A'}</Typography>
</Stack>
<Stack>
<Typography variant="body2" fontWeight={700}>
Value
</Typography>
<Typography variant="body2">{error.cell ?? 'N/A'}</Typography>
</Stack>
{(error.cell || error.header) && error.values && (
<Stack>
<Typography variant="body2" fontWeight={700}>
Allowed Values
</Typography>
<Typography variant="body2">{error.values.join(', ')}</Typography>
</Stack>
)}
</Stack>
</Stack>
}
/>
);
})}
{props.errors.length > MAX_ERRORS_SHOWN && (
<Stack direction="row" justifyContent="space-between" alignItems="center" mt={1}>
<IconButton onClick={handlePreviousPage} disabled={!currentPage}>
<Icon path={mdiChevronLeft} size={1} />
</IconButton>
<Typography variant="body2">
Page {currentPage + 1} of {pageCount}
</Typography>
<IconButton onClick={handleNextPage} disabled={currentPage + 1 === pageCount}>
<Icon path={mdiChevronRight} size={1} />
</IconButton>
</Stack>
)}
</Stack>
);
};
Original file line number Diff line number Diff line change
@@ -1,47 +1,40 @@
import { Divider, Paper, Toolbar, Typography } from '@mui/material';
import { Toolbar, Typography } from '@mui/material';
import { Box, Stack } from '@mui/system';
import { ReactElement } from 'react';
import { CSVError } from 'utils/csv-utils';
import { CSVErrorsTable } from './CSVErrorsTable';
import { CSVErrorsCardStack } from './CSVErrorsCardStack';

interface CSVErrorsTableContainerProps {
interface CSVErrorsCardStackContainerProps {
errors: CSVError[];
title?: ReactElement;
}

/**
* Renders a CSV errors table with toolbar.
*
* @param {CSVErrorsTableContainerProps} props
* @param {CSVErrorsCardStackContainerProps} props
* @returns {*} {JSX.Element}
*/
export const CSVErrorsTableContainer = (props: CSVErrorsTableContainerProps) => {
export const CSVErrorsCardStackContainer = (props: CSVErrorsCardStackContainerProps) => {
return (
<Paper component={Stack} flexDirection="column" flex="1 1 auto" height="100%">
<Toolbar
disableGutters
sx={{
pl: 2,
pr: 3
}}>
<Stack flexDirection="column" flex="1 1 auto" height="100%">
<Toolbar disableGutters>
{props.title ?? (
<Typography
sx={{
flexGrow: '1',
fontSize: '1.125rem',
fontWeight: 700
}}>
CSV Errors Detected &zwnj;
Errors &zwnj;
<Typography sx={{ fontWeight: '400' }} component="span" variant="inherit" color="textSecondary">
({props.errors.length})
</Typography>
</Typography>
)}
</Toolbar>
<Divider />
<Box width="100%" height="100%">
<CSVErrorsTable errors={props.errors} />
<CSVErrorsCardStack errors={props.errors} />
</Box>
</Paper>
</Stack>
);
};
Loading

0 comments on commit fedb4e9

Please sign in to comment.