Skip to content

Commit e2b60ce

Browse files
authored
Merge pull request #11 from openscript/develop
Enhance unique constraint implementation
2 parents 3a9e7ec + 28fed23 commit e2b60ce

15 files changed

+936
-767
lines changed

README.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,9 @@ The most important features of this component are:
6262
- ✅ Fully compositable
6363
- ✅ Automatic testing with >90% coverage
6464
- ✅ Input validation
65-
-[Material UI](https://material-ui.com/) integration
66-
-[ant.design](https://ant.design/) integration
65+
-[Ant Design](https://ant.design/) integration (see storybook)
66+
- ❌ Input transformation
67+
-[Material UI](https://material-ui.com/) integration (see storybook)
6768

6869
✅ means the feature is implemented and released. ❌ indicates that a feature is planned.
6970

docs/antd-integration.stories.mdx

+73
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Meta, Description, Source } from '@storybook/addon-docs/blocks';
2+
import Readme from '../README.md';
3+
4+
<Meta title="Usage|Integrations/Ant Design" />
5+
6+
# Ant Design integration
7+
This page shows how an input and a preview component with Ant Design (`>= 4`) can be built and connected to this component.
8+
9+
First the imports need to be declared.
10+
11+
<Source language='tsx' code={`
12+
import * as React from 'react';
13+
import { DSVImport as Import, ColumnsType, useDSVImport } from 'react-dsv-import';
14+
import { Form, Input, Table } from 'antd';
15+
`} />
16+
17+
## Input component
18+
<Source language='tsx' code={`
19+
const TextareaInput: React.FC = () => {
20+
const [, dispatch] = useDSVImport();
21+
22+
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
23+
dispatch({ type: 'setRaw', raw: event.target.value });
24+
};
25+
26+
return <Input.TextArea rows={15} onChange={handleChange} />;
27+
};
28+
`} />
29+
30+
## Preview component
31+
<Source language='tsx' code={`
32+
const TablePreview: React.FC = () => {
33+
const [context] = useDSVImport();
34+
35+
const getRowKey = (record: { [key: string]: string }) => {
36+
return context.parsed.indexOf(record);
37+
};
38+
39+
return (
40+
<Table pagination={false} dataSource={context.parsed} rowKey={getRowKey}>
41+
{context.columns.map((r) => {
42+
return <Table.Column key={r.key} dataIndex={r.key} title={r.label ? r.label : r.key} />;
43+
})}
44+
</Table>
45+
);
46+
};
47+
`} />
48+
49+
## Create context
50+
51+
<Source language='tsx' code={`
52+
export interface Props<T> {
53+
onChange?: (value: T[]) => void;
54+
columns: ColumnsType<T>;
55+
}
56+
57+
export const DSVImport = <T extends { [key: string]: string }>(props: Props<T>) => {
58+
const intl = useIntl();
59+
60+
return (
61+
<Form layout='vertical'>
62+
<Import<T> columns={props.columns} onChange={props.onChange}>
63+
<Form.Item label='Input'>
64+
<TextareaInput />
65+
</Form.Item>
66+
<Form.Item label='Preview'>
67+
<TablePreview />
68+
</Form.Item>
69+
</Import>
70+
</Form>
71+
);
72+
};
73+
`} />

docs/input.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
console.log('asdsa');

docs/material-integration.stories.mdx

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Meta, Description, Source } from '@storybook/addon-docs/blocks';
2+
3+
<Meta title="Usage|Integrations/Material UI" />
4+
5+
# Material UI integration
6+
This page shows how an input and a preview component with Material UI can be built and connected to this component. It's possible to put everything into one file.
7+
8+
First the imports need to be declared.
9+
10+
<Source language='tsx' code={`
11+
import * as React from 'react';
12+
import { DSVImport as Import, ColumnsType, useDSVImport } from 'react-dsv-import';
13+
`} />
14+
15+
## Input component
16+
17+
## Preview component
18+
19+
## Create context

docs/start.stories.mdx

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { Meta, Description } from '@storybook/addon-docs/blocks';
22
import Readme from '../README.md';
33

4-
5-
<Meta title="Start" />
4+
<Meta title="Start|Readme" />
65

76
<Description markdown={Readme} />

package.json

+14-14
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"react"
88
],
99
"homepage": "https://openscript.github.io/react-dsv-import/",
10-
"version": "0.2.2",
10+
"version": "0.2.3",
1111
"main": "dist/index.js",
1212
"module": "dist/es/index.js",
1313
"types": "dist/index.d.ts",
@@ -25,30 +25,30 @@
2525
"@storybook/addons": "^5.3.18",
2626
"@storybook/preset-typescript": "^3.0.0",
2727
"@storybook/react": "^5.3.18",
28-
"@testing-library/jest-dom": "^5.7.0",
28+
"@testing-library/jest-dom": "^5.8.0",
2929
"@testing-library/react": "^10.0.4",
3030
"@testing-library/react-hooks": "^3.2.1",
31-
"@types/jest": "^25.2.1",
32-
"@types/node": "^13.13.5",
33-
"@types/react": "^16.9.34",
34-
"@types/react-dom": "^16.9.7",
35-
"@typescript-eslint/eslint-plugin": "^2.31.0",
36-
"@typescript-eslint/parser": "^2.31.0",
31+
"@types/jest": "^25.2.3",
32+
"@types/node": "^14.0.4",
33+
"@types/react": "^16.9.35",
34+
"@types/react-dom": "^16.9.8",
35+
"@typescript-eslint/eslint-plugin": "^2.34.0",
36+
"@typescript-eslint/parser": "^2.34.0",
3737
"babel-loader": "^8.1.0",
3838
"babel-preset-react-app": "^9.1.2",
3939
"eslint": "^7.0.0",
4040
"eslint-config-prettier": "^6.11.0",
4141
"eslint-plugin-prettier": "^3.1.3",
42-
"eslint-plugin-react": "^7.19.0",
43-
"jest": "^25.5.4",
42+
"eslint-plugin-react": "^7.20.0",
43+
"jest": "^26.0.1",
4444
"prettier": "^2.0.5",
4545
"react-is": "^16.13.1",
4646
"react-test-renderer": "^16.13.1",
47-
"rollup": "^2.9.0",
48-
"ts-jest": "^25.5.1",
47+
"rollup": "^2.10.5",
48+
"ts-jest": "^26.0.0",
4949
"ts-node": "^8.10.1",
50-
"tslib": "^1.11.2",
51-
"typescript": "^3.8.3"
50+
"tslib": "^2.0.0",
51+
"typescript": "^3.9.3"
5252
},
5353
"scripts": {
5454
"build": "yarn build:rollup",

src/DSVImport.stories.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { DSVImport, ColumnsType } from './';
33
import { action } from '@storybook/addon-actions';
44
import styled from '@emotion/styled';
55

6-
export default { title: 'Usage' };
6+
export default { title: 'Usage|API' };
77

88
type BasicType = { forename: string; surname: string; email: string };
99

src/DSVImport.test.tsx

+4-1
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

+7-5
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,19 @@ 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' },
65+
{ column: 'email', row: 1, message: 'No example address, please' },
6466
{ column: 'forename', row: 1, message: 'Forename is required' }
6567
]
6668
});
6769
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');
7270

7371
expect(tableBody?.children[1].children[0]).toHaveClass('error');
7472
expect(tableBody?.children[1].children[0]).toHaveAttribute('title', 'Forename is required');
73+
expect(tableBody?.children[1].children[2]).toHaveAttribute(
74+
'title',
75+
'Contains duplicates;No example address, please'
76+
);
7577
});
7678
});

src/components/previews/TablePreview.tsx

+2-21
Original file line numberDiff line numberDiff line change
@@ -8,32 +8,15 @@ 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);
36-
const messages = errors?.map((e) => e?.message).join(';');
19+
const messages = errors?.map((e) => e.message).join(';');
3720

3821
return (
3922
<td className={messages ? 'error' : ''} title={messages}>
@@ -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

+1
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

+7-3
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ describe('validatorMiddleware', () => {
1313
const middleware = createValidatorMiddleware<TestType>();
1414
const parsed: TestType[] = [
1515
{ forename: 'Hans', surname: 'Muster', email: '[email protected]' },
16-
{ forename: 'Heidi', surname: 'Muster', email: '[email protected]' }
16+
{ forename: 'Heidi', surname: 'Muster', email: '[email protected]' },
17+
{ forename: 'Joe', surname: 'Doe', email: '[email protected]' }
1718
];
1819

1920
it('should return an empty array if there are no errors', () => {
@@ -34,7 +35,10 @@ describe('validatorMiddleware', () => {
3435

3536
expect(dispatchMock).toBeCalledWith({
3637
type: 'setValidation',
37-
errors: [{ column: 'email', message: 'Contains duplicates' }]
38+
errors: [
39+
{ column: 'email', row: 0, message: 'Contains duplicates' },
40+
{ column: 'email', row: 1, message: 'Contains duplicates' }
41+
]
3842
});
3943
});
4044

@@ -50,7 +54,7 @@ describe('validatorMiddleware', () => {
5054

5155
expect(dispatchMock).toBeCalledWith({
5256
type: 'setValidation',
53-
errors: [{ column: 'forename', row: 1, message: "No 'Hans' allowed" }]
57+
errors: [{ column: 'forename', row: 0, message: "No 'Hans' allowed" }]
5458
});
5559
});
5660
});

src/middlewares/validatorMiddleware.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import { Actions } from '../models/actions';
44
import { Rule, UniqueConstraint, CallbackConstraint } from '../models/rule';
55
import { ValidationError } from '../models/validation';
66

7-
const onlyUniqueValues = (data: string[]) => {
8-
return new Set(data).size === data.length;
7+
const onlyUniqueValues = (values: string[]) => {
8+
return new Set(values).size === values.length;
9+
};
10+
11+
const getDuplicates = (values: string[]) => {
12+
return Array.from(new Set(values.filter((item, index) => values.indexOf(item) != index)));
913
};
1014

1115
const validateColumn = <T>(key: keyof T, data: T[keyof T][], rules?: Rule[]): ValidationError<T>[] => {
@@ -15,11 +19,16 @@ const validateColumn = <T>(key: keyof T, data: T[keyof T][], rules?: Rule[]): Va
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 = getDuplicates(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) => {
22-
if (!callback(v)) {
31+
if (callback(v)) {
2332
errors.push({ column: key, row: i, message: r.message });
2433
}
2534
});

src/models/validation.ts

+1-1
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)