Skip to content

Commit 6dee52a

Browse files
committed
Show unique constraint errors on duplicates
1 parent eb4f5b1 commit 6dee52a

File tree

8 files changed

+24
-29
lines changed

8 files changed

+24
-29
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ The most important features of this component are:
6262
- ✅ Fully compositable
6363
- ✅ Automatic testing with >90% coverage
6464
- ✅ Input validation
65+
- ❌ Input transformation
6566
-[Material UI](https://material-ui.com/) integration
6667
-[ant.design](https://ant.design/) integration
6768

src/DSVImport.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ describe('DSVImport', () => {
6161
});
6262
}
6363

64-
expect(onValidationMock).toBeCalledWith([{ column: 'email', message: 'Contains duplicates' }]);
64+
expect(onValidationMock).toBeCalledWith([
65+
{ column: 'email', row: 0, message: 'Contains duplicates' },
66+
{ column: 'email', row: 1, message: 'Contains duplicates' }
67+
]);
6568
});
6669
});

src/components/previews/TablePreview.test.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,15 +60,12 @@ describe('TablePreview', () => {
6060
{ forename: '', surname: '', email: '[email protected]' }
6161
],
6262
validation: [
63-
{ column: 'email', message: 'Contains duplicates' },
63+
{ column: 'email', row: 0, message: 'Contains duplicates' },
64+
{ column: 'email', row: 1, message: 'Contains duplicates' },
6465
{ column: 'forename', row: 1, message: 'Forename is required' }
6566
]
6667
});
6768
const tableBody = container.querySelector('tbody');
68-
const tableHead = container.querySelector('thead tr');
69-
70-
expect(tableHead?.children[2]).toHaveClass('error');
71-
expect(tableHead?.children[2]).toHaveAttribute('title', 'Contains duplicates');
7269

7370
expect(tableBody?.children[1].children[0]).toHaveClass('error');
7471
expect(tableBody?.children[1].children[0]).toHaveAttribute('title', 'Forename is required');

src/components/previews/TablePreview.tsx

Lines changed: 1 addition & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,29 +8,12 @@ export interface TablePreviewProps {
88
export const TablePreview: React.FC<TablePreviewProps> = (props) => {
99
const [context] = useDSVImport();
1010

11-
const getColumnValidationError = (columnKey: string) => {
12-
if (context.validation) {
13-
return context.validation.filter((e) => e.column === columnKey && !e.row);
14-
}
15-
};
16-
1711
const getCellValidationError = (columnKey: string, rowIndex: number) => {
1812
if (context.validation) {
1913
return context.validation.filter((e) => e.column === columnKey && e.row === rowIndex);
2014
}
2115
};
2216

23-
const ColumnHead: React.FC<{ columnKey: string }> = (props) => {
24-
const errors = getColumnValidationError(props.columnKey);
25-
const messages = errors?.map((e) => e?.message).join(';');
26-
27-
return (
28-
<th className={messages ? 'error' : ''} title={messages}>
29-
{props.children}
30-
</th>
31-
);
32-
};
33-
3417
const Cell: React.FC<{ columnKey: string; rowIndex: number }> = (props) => {
3518
const errors = getCellValidationError(props.columnKey, props.rowIndex);
3619
const messages = errors?.map((e) => e?.message).join(';');
@@ -47,9 +30,7 @@ export const TablePreview: React.FC<TablePreviewProps> = (props) => {
4730
<thead>
4831
<tr>
4932
{context.columns.map((column, columnIndex) => (
50-
<ColumnHead key={columnIndex} columnKey={column.key.toString()}>
51-
{column.label}
52-
</ColumnHead>
33+
<th key={columnIndex}>{column.label}</th>
5334
))}
5435
</tr>
5536
</thead>

src/index.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ DSVImport.TablePreview = TablePreview;
1212

1313
export { ColumnsType } from './models/column';
1414
export { useDSVImport } from './features/context';
15+
export { Rule } from './models/rule';

src/middlewares/validatorMiddleware.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@ describe('validatorMiddleware', () => {
3434

3535
expect(dispatchMock).toBeCalledWith({
3636
type: 'setValidation',
37-
errors: [{ column: 'email', message: 'Contains duplicates' }]
37+
errors: [
38+
{ column: 'email', row: 0, message: 'Contains duplicates' },
39+
{ column: 'email', row: 1, message: 'Contains duplicates' }
40+
]
3841
});
3942
});
4043

src/middlewares/validatorMiddleware.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,23 @@ const onlyUniqueValues = (data: string[]) => {
88
return new Set(data).size === data.length;
99
};
1010

11+
const findDuplicates = (data: string[]) => {
12+
return Array.from(new Set(data.filter((item, index) => data.indexOf(item) != index)));
13+
};
14+
1115
const validateColumn = <T>(key: keyof T, data: T[keyof T][], rules?: Rule[]): ValidationError<T>[] => {
1216
const errors: ValidationError<T>[] = [];
1317

1418
if (rules) {
1519
const values = data.map((d) => new String(d).toString());
1620
rules.forEach((r) => {
1721
if ((r.constraint as UniqueConstraint).unique && !onlyUniqueValues(values)) {
18-
errors.push({ column: key, message: r.message });
22+
const duplicates = findDuplicates(values);
23+
values.forEach((v, i) => {
24+
if (duplicates.indexOf(v) !== -1) {
25+
errors.push({ column: key, row: i, message: r.message });
26+
}
27+
});
1928
} else if (typeof (r.constraint as CallbackConstraint).callback === 'function') {
2029
const callback = (r.constraint as CallbackConstraint).callback;
2130
values.forEach((v, i) => {

src/models/validation.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export type ValidationError<T> = { column: keyof T; row?: number; message: string };
1+
export type ValidationError<T> = { column: keyof T; row: number; message: string };

0 commit comments

Comments
 (0)