Skip to content

Commit

Permalink
Merge pull request #11 from openscript/develop
Browse files Browse the repository at this point in the history
Enhance unique constraint implementation
  • Loading branch information
openscript authored May 20, 2020
2 parents 3a9e7ec + 28fed23 commit e2b60ce
Show file tree
Hide file tree
Showing 15 changed files with 936 additions and 767 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ The most important features of this component are:
- ✅ Fully compositable
- ✅ Automatic testing with >90% coverage
- ✅ Input validation
-[Material UI](https://material-ui.com/) integration
-[ant.design](https://ant.design/) integration
-[Ant Design](https://ant.design/) integration (see storybook)
- ❌ Input transformation
-[Material UI](https://material-ui.com/) integration (see storybook)

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

Expand Down
73 changes: 73 additions & 0 deletions docs/antd-integration.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Meta, Description, Source } from '@storybook/addon-docs/blocks';
import Readme from '../README.md';

<Meta title="Usage|Integrations/Ant Design" />

# Ant Design integration
This page shows how an input and a preview component with Ant Design (`>= 4`) can be built and connected to this component.

First the imports need to be declared.

<Source language='tsx' code={`
import * as React from 'react';
import { DSVImport as Import, ColumnsType, useDSVImport } from 'react-dsv-import';
import { Form, Input, Table } from 'antd';
`} />

## Input component
<Source language='tsx' code={`
const TextareaInput: React.FC = () => {
const [, dispatch] = useDSVImport();
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
dispatch({ type: 'setRaw', raw: event.target.value });
};
return <Input.TextArea rows={15} onChange={handleChange} />;
};
`} />

## Preview component
<Source language='tsx' code={`
const TablePreview: React.FC = () => {
const [context] = useDSVImport();
const getRowKey = (record: { [key: string]: string }) => {
return context.parsed.indexOf(record);
};
return (
<Table pagination={false} dataSource={context.parsed} rowKey={getRowKey}>
{context.columns.map((r) => {
return <Table.Column key={r.key} dataIndex={r.key} title={r.label ? r.label : r.key} />;
})}
</Table>
);
};
`} />

## Create context

<Source language='tsx' code={`
export interface Props<T> {
onChange?: (value: T[]) => void;
columns: ColumnsType<T>;
}
export const DSVImport = <T extends { [key: string]: string }>(props: Props<T>) => {
const intl = useIntl();
return (
<Form layout='vertical'>
<Import<T> columns={props.columns} onChange={props.onChange}>
<Form.Item label='Input'>
<TextareaInput />
</Form.Item>
<Form.Item label='Preview'>
<TablePreview />
</Form.Item>
</Import>
</Form>
);
};
`} />
1 change: 1 addition & 0 deletions docs/input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
console.log('asdsa');
19 changes: 19 additions & 0 deletions docs/material-integration.stories.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Meta, Description, Source } from '@storybook/addon-docs/blocks';

<Meta title="Usage|Integrations/Material UI" />

# Material UI integration
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.

First the imports need to be declared.

<Source language='tsx' code={`
import * as React from 'react';
import { DSVImport as Import, ColumnsType, useDSVImport } from 'react-dsv-import';
`} />

## Input component

## Preview component

## Create context
3 changes: 1 addition & 2 deletions docs/start.stories.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Meta, Description } from '@storybook/addon-docs/blocks';
import Readme from '../README.md';


<Meta title="Start" />
<Meta title="Start|Readme" />

<Description markdown={Readme} />
28 changes: 14 additions & 14 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"react"
],
"homepage": "https://openscript.github.io/react-dsv-import/",
"version": "0.2.2",
"version": "0.2.3",
"main": "dist/index.js",
"module": "dist/es/index.js",
"types": "dist/index.d.ts",
Expand All @@ -25,30 +25,30 @@
"@storybook/addons": "^5.3.18",
"@storybook/preset-typescript": "^3.0.0",
"@storybook/react": "^5.3.18",
"@testing-library/jest-dom": "^5.7.0",
"@testing-library/jest-dom": "^5.8.0",
"@testing-library/react": "^10.0.4",
"@testing-library/react-hooks": "^3.2.1",
"@types/jest": "^25.2.1",
"@types/node": "^13.13.5",
"@types/react": "^16.9.34",
"@types/react-dom": "^16.9.7",
"@typescript-eslint/eslint-plugin": "^2.31.0",
"@typescript-eslint/parser": "^2.31.0",
"@types/jest": "^25.2.3",
"@types/node": "^14.0.4",
"@types/react": "^16.9.35",
"@types/react-dom": "^16.9.8",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"babel-loader": "^8.1.0",
"babel-preset-react-app": "^9.1.2",
"eslint": "^7.0.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-react": "^7.19.0",
"jest": "^25.5.4",
"eslint-plugin-react": "^7.20.0",
"jest": "^26.0.1",
"prettier": "^2.0.5",
"react-is": "^16.13.1",
"react-test-renderer": "^16.13.1",
"rollup": "^2.9.0",
"ts-jest": "^25.5.1",
"rollup": "^2.10.5",
"ts-jest": "^26.0.0",
"ts-node": "^8.10.1",
"tslib": "^1.11.2",
"typescript": "^3.8.3"
"tslib": "^2.0.0",
"typescript": "^3.9.3"
},
"scripts": {
"build": "yarn build:rollup",
Expand Down
2 changes: 1 addition & 1 deletion src/DSVImport.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { DSVImport, ColumnsType } from './';
import { action } from '@storybook/addon-actions';
import styled from '@emotion/styled';

export default { title: 'Usage' };
export default { title: 'Usage|API' };

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

Expand Down
5 changes: 4 additions & 1 deletion src/DSVImport.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ describe('DSVImport', () => {
});
}

expect(onValidationMock).toBeCalledWith([{ column: 'email', message: 'Contains duplicates' }]);
expect(onValidationMock).toBeCalledWith([
{ column: 'email', row: 0, message: 'Contains duplicates' },
{ column: 'email', row: 1, message: 'Contains duplicates' }
]);
});
});
12 changes: 7 additions & 5 deletions src/components/previews/TablePreview.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,19 @@ describe('TablePreview', () => {
{ forename: '', surname: '', email: '[email protected]' }
],
validation: [
{ column: 'email', message: 'Contains duplicates' },
{ column: 'email', row: 0, message: 'Contains duplicates' },
{ column: 'email', row: 1, message: 'Contains duplicates' },
{ column: 'email', row: 1, message: 'No example address, please' },
{ column: 'forename', row: 1, message: 'Forename is required' }
]
});
const tableBody = container.querySelector('tbody');
const tableHead = container.querySelector('thead tr');

expect(tableHead?.children[2]).toHaveClass('error');
expect(tableHead?.children[2]).toHaveAttribute('title', 'Contains duplicates');

expect(tableBody?.children[1].children[0]).toHaveClass('error');
expect(tableBody?.children[1].children[0]).toHaveAttribute('title', 'Forename is required');
expect(tableBody?.children[1].children[2]).toHaveAttribute(
'title',
'Contains duplicates;No example address, please'
);
});
});
23 changes: 2 additions & 21 deletions src/components/previews/TablePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,32 +8,15 @@ export interface TablePreviewProps {
export const TablePreview: React.FC<TablePreviewProps> = (props) => {
const [context] = useDSVImport();

const getColumnValidationError = (columnKey: string) => {
if (context.validation) {
return context.validation.filter((e) => e.column === columnKey && !e.row);
}
};

const getCellValidationError = (columnKey: string, rowIndex: number) => {
if (context.validation) {
return context.validation.filter((e) => e.column === columnKey && e.row === rowIndex);
}
};

const ColumnHead: React.FC<{ columnKey: string }> = (props) => {
const errors = getColumnValidationError(props.columnKey);
const messages = errors?.map((e) => e?.message).join(';');

return (
<th className={messages ? 'error' : ''} title={messages}>
{props.children}
</th>
);
};

const Cell: React.FC<{ columnKey: string; rowIndex: number }> = (props) => {
const errors = getCellValidationError(props.columnKey, props.rowIndex);
const messages = errors?.map((e) => e?.message).join(';');
const messages = errors?.map((e) => e.message).join(';');

return (
<td className={messages ? 'error' : ''} title={messages}>
Expand All @@ -47,9 +30,7 @@ export const TablePreview: React.FC<TablePreviewProps> = (props) => {
<thead>
<tr>
{context.columns.map((column, columnIndex) => (
<ColumnHead key={columnIndex} columnKey={column.key.toString()}>
{column.label}
</ColumnHead>
<th key={columnIndex}>{column.label}</th>
))}
</tr>
</thead>
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ DSVImport.TablePreview = TablePreview;

export { ColumnsType } from './models/column';
export { useDSVImport } from './features/context';
export { Rule } from './models/rule';
10 changes: 7 additions & 3 deletions src/middlewares/validatorMiddleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ describe('validatorMiddleware', () => {
const middleware = createValidatorMiddleware<TestType>();
const parsed: TestType[] = [
{ forename: 'Hans', surname: 'Muster', email: '[email protected]' },
{ forename: 'Heidi', surname: 'Muster', email: '[email protected]' }
{ forename: 'Heidi', surname: 'Muster', email: '[email protected]' },
{ forename: 'Joe', surname: 'Doe', email: '[email protected]' }
];

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

expect(dispatchMock).toBeCalledWith({
type: 'setValidation',
errors: [{ column: 'email', message: 'Contains duplicates' }]
errors: [
{ column: 'email', row: 0, message: 'Contains duplicates' },
{ column: 'email', row: 1, message: 'Contains duplicates' }
]
});
});

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

expect(dispatchMock).toBeCalledWith({
type: 'setValidation',
errors: [{ column: 'forename', row: 1, message: "No 'Hans' allowed" }]
errors: [{ column: 'forename', row: 0, message: "No 'Hans' allowed" }]
});
});
});
17 changes: 13 additions & 4 deletions src/middlewares/validatorMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ import { Actions } from '../models/actions';
import { Rule, UniqueConstraint, CallbackConstraint } from '../models/rule';
import { ValidationError } from '../models/validation';

const onlyUniqueValues = (data: string[]) => {
return new Set(data).size === data.length;
const onlyUniqueValues = (values: string[]) => {
return new Set(values).size === values.length;
};

const getDuplicates = (values: string[]) => {
return Array.from(new Set(values.filter((item, index) => values.indexOf(item) != index)));
};

const validateColumn = <T>(key: keyof T, data: T[keyof T][], rules?: Rule[]): ValidationError<T>[] => {
Expand All @@ -15,11 +19,16 @@ const validateColumn = <T>(key: keyof T, data: T[keyof T][], rules?: Rule[]): Va
const values = data.map((d) => new String(d).toString());
rules.forEach((r) => {
if ((r.constraint as UniqueConstraint).unique && !onlyUniqueValues(values)) {
errors.push({ column: key, message: r.message });
const duplicates = getDuplicates(values);
values.forEach((v, i) => {
if (duplicates.indexOf(v) !== -1) {
errors.push({ column: key, row: i, message: r.message });
}
});
} else if (typeof (r.constraint as CallbackConstraint).callback === 'function') {
const callback = (r.constraint as CallbackConstraint).callback;
values.forEach((v, i) => {
if (!callback(v)) {
if (callback(v)) {
errors.push({ column: key, row: i, message: r.message });
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/models/validation.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export type ValidationError<T> = { column: keyof T; row?: number; message: string };
export type ValidationError<T> = { column: keyof T; row: number; message: string };
Loading

0 comments on commit e2b60ce

Please sign in to comment.