diff --git a/docs/FAQ.md b/docs/FAQ.md index 0d0b5058cbc..c46daefce27 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -11,7 +11,6 @@ title: "FAQ" - [How can I customize the UI depending on the user permissions?](#how-can-i-customize-the-ui-depending-on-the-user-permissions) - [How can I customize forms depending on its inputs values?](#how-can-i-customize-forms-depending-on-its-inputs-values) - [My Resource is defined but not displayed on the Menu](#my-resource-is-defined-but-not-displayed-on-the-menu) -- [Why React Admin Doesn't Support The Latest Version Of Material-UI?](#why-react-admin-doesnt-support-the-latest-version-of-material-ui) ## Can I have custom identifiers/primary keys for my resources? @@ -133,13 +132,3 @@ In order to have a specific resource without `list` prop listed on the menu, you ); ``` - -## Why Doesn't React Admin Support The Latest Version Of Material-UI? - -React Admin users and third-party libraries maintainers might have noticed that the default UI template `ra-ui-materialui` [has `@material-ui/core@^1.4.0` as dependency](https://github.com/marmelab/react-admin/blob/ae45a2509b391a6ea81cdf9c248ff9d28364b6e1/packages/ra-ui-materialui/package.json#L44) even though the latest version of Material UI is already 3.x. - -We chose not to upgrade to Material UI v3 when it was released because the MUI team was already hard at work preparing the next major version ([which includes major breaking changes](https://github.com/mui-org/material-ui/issues/13663)). In fact, material-ui published a release schedule for one major version every 6 months. This means that developers using material-ui have to upgrade their codebase every six months to get the latest updates. On the other hand, react-admin plans to release a major version once every year, minimizing the upgrade work for developers. This gain in stability is a tradeoff - react-admin users can't use the latest version of material-ui for about half a year. - -Feel free to discuss this policy in [issue #2399](https://github.com/marmelab/react-admin/issues/2399). - -If you are a maintainer of a third-party library based on React Admin, your library has to add material-ui v1.x as a peer dependency. diff --git a/examples/demo/package.json b/examples/demo/package.json index bb6f8316835..6fb342286a2 100644 --- a/examples/demo/package.json +++ b/examples/demo/package.json @@ -3,8 +3,8 @@ "version": "0.1.0", "private": true, "dependencies": { - "@material-ui/core": "~1.5.1", - "@material-ui/icons": "~1.1.1", + "@material-ui/core": "~3.9.3", + "@material-ui/icons": "~3.0.2", "data-generator-retail": "^2.7.0", "fakerest": "~2.1.0", "fetch-mock": "~6.3.0", diff --git a/examples/simple/package.json b/examples/simple/package.json index 16660aa55a3..0f13f30ffc8 100644 --- a/examples/simple/package.json +++ b/examples/simple/package.json @@ -37,8 +37,8 @@ "webpack-dev-server": "~3.1.11" }, "dependencies": { - "@material-ui/core": "~1.5.1", - "@material-ui/icons": "~1.1.1", + "@material-ui/core": "~3.9.3", + "@material-ui/icons": "~3.0.2", "@babel/polyfill": "^7.0.0", "ra-data-fakerest": "^2.0.0", "ra-input-rich-text": "^2.0.0", diff --git a/examples/tutorial/package.json b/examples/tutorial/package.json index 8b2d8965d42..f33eaa1fb50 100644 --- a/examples/tutorial/package.json +++ b/examples/tutorial/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { - "@material-ui/core": "~1.5.1", + "@material-ui/core": "~3.9.3", "ra-data-json-server": "^2.0.0", "react": "~16.3.1", "react-admin": "^2.0.0", diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index 2031b884cdd..9d60fae21b6 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -26,8 +26,8 @@ "watch": "rimraf ./lib && tsc --watch" }, "devDependencies": { - "@material-ui/core": "~1.5.1", - "@material-ui/icons": "^1.1.1", + "@material-ui/core": "~3.9.3", + "@material-ui/icons": "~3.0.2", "cross-env": "^5.2.0", "enzyme": "~3.7.0", "enzyme-adapter-react-16": "~1.6.0", @@ -43,8 +43,8 @@ "react-dom": "^16.3.0" }, "dependencies": { - "@material-ui/core": "^1.4.0", - "@material-ui/icons": "^1.0.0", + "@material-ui/core": "^1.4.0 || ^3.0.0", + "@material-ui/icons": "^1.0.0 || ^3.0.0", "autosuggest-highlight": "^3.1.1", "classnames": "~2.2.5", "inflection": "~1.12.0", diff --git a/packages/ra-ui-materialui/src/auth/Login.tsx b/packages/ra-ui-materialui/src/auth/Login.tsx index 16939e709d9..d349cfc2368 100644 --- a/packages/ra-ui-materialui/src/auth/Login.tsx +++ b/packages/ra-ui-materialui/src/auth/Login.tsx @@ -27,7 +27,6 @@ interface Props { backgroundImage?: string; loginForm: ReactElement; theme: object; - staticContext: StaticContext; } const styles = (theme: Theme) => @@ -77,18 +76,6 @@ const styles = (theme: Theme) => class Login extends Component< Props & WithStyles & HtmlHTMLAttributes > { - static propTypes = { - backgroundImage: PropTypes.string, - loginForm: PropTypes.element, - theme: PropTypes.object, - }; - - static defaultProps = { - backgroundImage: 'https://source.unsplash.com/random/1600x900/daily', - theme: defaultTheme, - loginForm: , - }; - theme = createMuiTheme(this.props.theme); containerRef = React.createRef(); backgroundImageLoaded = false; @@ -128,7 +115,6 @@ class Login extends Component< classes, className, loginForm, - staticContext, ...rest } = this.props; @@ -156,4 +142,15 @@ class Login extends Component< const EnhancedLogin = withStyles(styles)(Login) as ComponentType; +EnhancedLogin.propTypes = { + backgroundImage: PropTypes.string, + loginForm: PropTypes.element, + theme: PropTypes.object, +}; + +EnhancedLogin.defaultProps = { + backgroundImage: 'https://source.unsplash.com/random/1600x900/daily', + theme: defaultTheme, + loginForm: , +}; export default EnhancedLogin; diff --git a/packages/ra-ui-materialui/src/field/BooleanField.js b/packages/ra-ui-materialui/src/field/BooleanField.js index 6e59ac06680..0e4559d046e 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.js +++ b/packages/ra-ui-materialui/src/field/BooleanField.js @@ -60,7 +60,7 @@ export const BooleanField = ({ {...sanitizeRestProps(rest)} > {ariaLabel} - + ); } @@ -74,7 +74,7 @@ export const BooleanField = ({ {...sanitizeRestProps(rest)} > {ariaLabel} - + ); } diff --git a/packages/ra-ui-materialui/src/field/BooleanField.spec.js b/packages/ra-ui-materialui/src/field/BooleanField.spec.js index cdf7e2d856c..25b1db14d6b 100644 --- a/packages/ra-ui-materialui/src/field/BooleanField.spec.js +++ b/packages/ra-ui-materialui/src/field/BooleanField.spec.js @@ -1,30 +1,25 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; import { BooleanField } from './BooleanField'; +import { render, cleanup } from 'react-testing-library'; describe('', () => { + afterEach(cleanup); it('should display tick and truthy text if value is true', () => { - const wrapper = shallow( + const { queryByText, getByRole } = render( ); - assert.ok(wrapper.first().is('WithStyles(Typography)')); - assert.equal(wrapper.first().find('pure(Done)').length, 1); - assert.equal( - wrapper - .first() - .find('span') - .text(), - 'ra.boolean.true' - ); + expect(queryByText('ra.boolean.true')).not.toBeNull(); + expect(getByRole('presentation').dataset.testid).toBe('true'); + expect(queryByText('ra.boolean.false')).toBeNull(); }); - it('should display tick and custom truthy text if value is true', () => { - const wrapper = shallow( + it('should use valueLabelTrue for custom truthy text', () => { + const { queryByText } = render( ', () => { valueLabelTrue="Has been published" /> ); - assert.ok(wrapper.first().is('WithStyles(Typography)')); - assert.equal(wrapper.first().find('pure(Done)').length, 1); - assert.equal( - wrapper - .first() - .find('span') - .text(), - 'Has been published' - ); + expect(queryByText('ra.boolean.true')).toBeNull(); + expect(queryByText('Has been published')).not.toBeNull(); }); it('should display cross and falsy text if value is false', () => { - const wrapper = shallow( + const { queryByText, getByRole } = render( ); - - assert.ok(wrapper.first().is('WithStyles(Typography)')); - assert.equal(wrapper.first().find('pure(Clear)').length, 1); - assert.equal( - wrapper - .first() - .find('span') - .text(), - 'ra.boolean.false' - ); + expect(queryByText('ra.boolean.true')).toBeNull(); + expect(getByRole('presentation').dataset.testid).toBe('false'); + expect(queryByText('ra.boolean.false')).not.toBeNull(); }); - it('should display tick and custom falsy text if value is true', () => { - const wrapper = shallow( + it('should use valueLabelFalse for custom falsy text', () => { + const { queryByText } = render( ); - assert.ok(wrapper.first().is('WithStyles(Typography)')); - assert.equal(wrapper.first().find('pure(Clear)').length, 1); - assert.equal( - wrapper - .first() - .find('span') - .text(), - 'Has not been published yet' - ); + expect(queryByText('ra.boolean.false')).toBeNull(); + expect(queryByText('Has not been published')).not.toBeNull(); }); it('should not display anything if value is null', () => { - const wrapper = shallow( + const { queryByText } = render( ); - - assert.equal(wrapper.first().children().length, 0); + expect(queryByText('ra.boolean.true')).toBeNull(); + expect(queryByText('ra.boolean.false')).toBeNull(); }); - it('should use custom className', () => - assert.deepEqual( - shallow( - - ).prop('className'), - 'foo' - )); + it('should use custom className', () => { + const { container } = render( + + ); + expect(container.firstChild.classList.contains('foo')).toBe(true); + }); it('should handle deep fields', () => { - const wrapper = shallow( + const { queryByText } = render( ); - assert.ok(wrapper.first().is('WithStyles(Typography)')); - assert.equal(wrapper.first().find('pure(Done)').length, 1); + expect(queryByText('ra.boolean.true')).not.toBeNull(); }); }); diff --git a/packages/ra-ui-materialui/src/input/BooleanInput.spec.js b/packages/ra-ui-materialui/src/input/BooleanInput.spec.js index f598465679f..0a87e5b5cc7 100644 --- a/packages/ra-ui-materialui/src/input/BooleanInput.spec.js +++ b/packages/ra-ui-materialui/src/input/BooleanInput.spec.js @@ -1,44 +1,43 @@ -import assert from 'assert'; -import { shallow } from 'enzyme'; import React from 'react'; +import expect from 'expect'; +import { render, cleanup } from 'react-testing-library'; import { BooleanInput } from './BooleanInput'; describe('', () => { - it('should render as a mui Toggle', () => { - const wrapper = shallow() - .find('WithStyles(FormControlLabel)') - .shallow() - .dive(); - const choices = wrapper.find('WithStyles(Switch)'); - assert.equal(choices.length, 1); + afterEach(cleanup); + + it('should render as a checkbox', () => { + const { getByLabelText } = render( + + ); + expect(getByLabelText('resources.foo.fields.bar').type).toBe( + 'checkbox' + ); }); it('should be checked if the value is true', () => { - const wrapper = shallow( - - ) - .find('WithStyles(FormControlLabel)') - .shallow() - .dive(); - assert.equal(wrapper.find('WithStyles(Switch)').prop('checked'), true); + const { getByLabelText } = render( + + ); + expect(getByLabelText('resources.foo.fields.bar').checked).toBe(true); }); it('should not be checked if the value is false', () => { - const wrapper = shallow( - - ) - .find('WithStyles(FormControlLabel)') - .shallow() - .dive(); - assert.equal(wrapper.find('WithStyles(Switch)').prop('checked'), false); + const { getByLabelText } = render( + + ); + expect(getByLabelText('resources.foo.fields.bar').checked).toBe(false); }); it('should not be checked if the value is undefined', () => { - const wrapper = shallow() - .find('WithStyles(FormControlLabel)') - .shallow() - .dive(); - assert.equal(wrapper.find('WithStyles(Switch)').prop('checked'), false); + const { getByLabelText } = render( + + ); + expect(getByLabelText('resources.foo.fields.bar').checked).toBe(false); }); }); diff --git a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.js b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.js index 0eda3d97f00..d5a9b6aab74 100644 --- a/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.js +++ b/packages/ra-ui-materialui/src/input/CheckboxGroupInput.spec.js @@ -1,7 +1,7 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; import { CheckboxGroupInput } from './CheckboxGroupInput'; +import { render, cleanup } from 'react-testing-library'; describe('', () => { const defaultProps = { @@ -13,298 +13,191 @@ describe('', () => { value: [], }, translate: x => x, - muiTheme: { - baseTheme: {}, - textField: {}, - prepareStyles: () => {}, - }, }; - it('should use a mui Checkbox', () => { - const wrapper = shallow(); - const CheckboxElement = wrapper - .find('WithStyles(FormControlLabel)') - .shallow() - .dive() - .find('WithStyles(Checkbox)'); - assert.equal(CheckboxElement.length, 1); - }); + afterEach(cleanup); - it('should use the input parameter value as the initial input value', () => { - const wrapper = shallow( + it('should render choices as checkbox components', () => { + const { getByLabelText } = render( {} }} + choices={[ + { id: 'ang', name: 'Angular' }, + { id: 'rct', name: 'React' }, + ]} /> ); - const CheckboxElement = wrapper - .find('WithStyles(FormControlLabel)') - .shallow() - .dive() - .find('WithStyles(Checkbox)') - .first(); - assert.equal(CheckboxElement.prop('checked'), true); + const input1 = getByLabelText('Angular'); + expect(input1.type).toBe('checkbox'); + expect(input1.value).toBe('ang'); + expect(input1.checked).toBe(false); + const input2 = getByLabelText('React'); + expect(input2.type).toBe('checkbox'); + expect(input2.value).toBe('rct'); + expect(input2.checked).toBe(false); }); - it('should render choices as mui Checkbox components', () => { - const wrapper = shallow( + it('should use the input parameter value as the initial input value', () => { + const { getByLabelText } = render( {} }} /> ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - assert.equal(CheckboxElements.length, 2); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'ang'); - assert.equal(CheckboxElement1.prop('label'), 'Angular'); - const CheckboxElement2 = CheckboxElements.at(1); - assert.equal(CheckboxElement2.prop('value'), 'rct'); - assert.equal(CheckboxElement2.prop('label'), 'React'); + const input1 = getByLabelText('Angular'); + expect(input1.checked).toBe(true); + const input2 = getByLabelText('React'); + expect(input2.checked).toBe(false); }); it('should use optionValue as value identifier', () => { - const wrapper = shallow( + const { getByLabelText } = render( ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'foo'); - assert.equal(CheckboxElement1.prop('label'), 'Bar'); + expect(getByLabelText('Bar').value).toBe('foo'); }); it('should use optionValue including "." as value identifier', () => { - const wrapper = shallow( + const { getByLabelText } = render( ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'foo'); - assert.equal(CheckboxElement1.prop('label'), 'Bar'); + expect(getByLabelText('Bar').value).toBe('foo'); }); it('should use optionText with a string value as text identifier', () => { - const wrapper = shallow( + const { queryByLabelText } = render( ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'foo'); - assert.equal(CheckboxElement1.prop('label'), 'Bar'); + expect(queryByLabelText('Bar')).not.toBeNull(); }); it('should use optionText with a string value including "." as text identifier', () => { - const wrapper = shallow( + const { queryByLabelText } = render( ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'foo'); - assert.equal(CheckboxElement1.prop('label'), 'Bar'); + expect(queryByLabelText('Bar')).not.toBeNull(); }); it('should use optionText with a function value as text identifier', () => { - const wrapper = shallow( + const { queryByLabelText } = render( choice.foobar} choices={[{ id: 'foo', foobar: 'Bar' }]} /> ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'foo'); - assert.equal(CheckboxElement1.prop('label'), 'Bar'); + expect(queryByLabelText('Bar')).not.toBeNull(); }); it('should use optionText with an element value as text identifier', () => { - const Foobar = ({ record }) => {record.foobar}; - const wrapper = shallow( + const Foobar = ({ record }) => ( + {record.foobar} + ); + const { queryByLabelText, queryByTestId } = render( } choices={[{ id: 'foo', foobar: 'Bar' }]} /> ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('value'), 'foo'); - assert.deepEqual( - CheckboxElement1.prop('label'), - - ); + expect(queryByLabelText('Bar')).not.toBeNull(); + expect(queryByTestId('label')).not.toBeNull(); }); it('should translate the choices by default', () => { - const wrapper = shallow( - `**${x}**`} - /> + const { queryByLabelText } = render( + `**${x}**`} /> ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('label'), '**Male**'); + expect(queryByLabelText('**John doe**')).not.toBeNull(); }); it('should not translate the choices if translateChoice is false', () => { - const wrapper = shallow( + const { queryByLabelText } = render( `**${x}**`} translateChoice={false} /> ); - const CheckboxElements = wrapper.find('WithStyles(FormControlLabel)'); - const CheckboxElement1 = CheckboxElements.first(); - assert.equal(CheckboxElement1.prop('label'), 'Male'); + expect(queryByLabelText('**John doe**')).toBeNull(); + expect(queryByLabelText('John doe')).not.toBeNull(); }); it('should displayed helperText if prop is present in meta', () => { - const wrapper = shallow( + const { queryByText } = render( `**${x}**`} - translateChoice={false} - meta={{ helperText: 'Can i help you?' }} + meta={{ helperText: 'Can I help you?' }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 1); - assert.equal( - FormHelperTextElement.children().text(), - 'Can i help you?' - ); + expect(queryByText('Can I help you?')).not.toBeNull(); }); describe('error message', () => { it('should not be displayed if field is pristine', () => { - const wrapper = shallow( + const { container } = render( `**${x}**`} - translateChoice={false} - meta={{ touched: false }} + meta={{ touched: false, error: 'Required field.' }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 0); + expect(container.querySelector('p')).toBeNull(); }); it('should not be displayed if field has been touched but is valid', () => { - const wrapper = shallow( + const { container } = render( `**${x}**`} - translateChoice={false} meta={{ touched: true, error: false }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 0); + expect(container.querySelector('p')).toBeNull(); }); it('should be displayed if field has been touched and is invalid', () => { - const wrapper = shallow( + const { container, queryByText } = render( `**${x}**`} - translateChoice={false} meta={{ touched: true, error: 'Required field.' }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 1); - assert.equal( - FormHelperTextElement.children().text(), - 'Required field.' - ); + expect(container.querySelector('p')).not.toBeNull(); + expect(queryByText('Required field.')).not.toBeNull(); }); it('should display the error and help text if helperText is present', () => { - const wrapper = shallow( + const { queryByText } = render( `**${x}**`} - translateChoice={false} meta={{ touched: true, error: 'Required field.', - helperText: 'Can i help you?', + helperText: 'Can I help you?', }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 2); - assert.equal( - FormHelperTextElement.at(0) - .children(0) - .text(), - 'Required field.' - ); - assert.equal( - FormHelperTextElement.at(1) - .children(0) - .text(), - 'Can i help you?' - ); + expect(queryByText('Required field.')).not.toBeNull(); + expect(queryByText('Can I help you?')).not.toBeNull(); }); }); }); diff --git a/packages/ra-ui-materialui/src/input/DateInput.js b/packages/ra-ui-materialui/src/input/DateInput.js index 9098735c3ae..2803995a667 100644 --- a/packages/ra-ui-materialui/src/input/DateInput.js +++ b/packages/ra-ui-materialui/src/input/DateInput.js @@ -72,6 +72,7 @@ export class DateInput extends Component { className={className} type="date" margin="normal" + id={`${resource}_${source}_date_input`} error={!!(touched && error)} helperText={touched && error} label={ diff --git a/packages/ra-ui-materialui/src/input/DateInput.spec.js b/packages/ra-ui-materialui/src/input/DateInput.spec.js index 00f29d814c4..a5ee7d15fe0 100644 --- a/packages/ra-ui-materialui/src/input/DateInput.spec.js +++ b/packages/ra-ui-materialui/src/input/DateInput.spec.js @@ -1,71 +1,62 @@ -import assert from 'assert'; -import { shallow } from 'enzyme'; import React from 'react'; +import expect from 'expect'; +import { render, fireEvent, cleanup } from 'react-testing-library'; import { DateInput } from './DateInput'; describe('', () => { - it('should render a localized ', () => { - const input = { value: null }; - const wrapper = shallow( - - ); - const datePicker = wrapper.find('TextField'); - assert.equal(datePicker.length, 1); - assert.equal(datePicker.first().prop('type'), 'date'); + const defaultProps = { + resource: 'bar', + source: 'foo', + meta: {}, + input: {}, + translate: x => x, + }; + + afterEach(cleanup); + + it('should render a date input', () => { + const { getByLabelText } = render(); + expect(getByLabelText('resources.bar.fields.foo').type).toBe('date'); }); - it('should call props `input.onChange` method when changed', () => { - const input = { value: null, onChange: jest.fn(), onBlur: () => {} }; - const wrapper = shallow( - - ) - .shallow() - .find('WithStyles(Input)') - .shallow() - .shallow() - .find('input'); - wrapper.simulate('change', { - target: { value: '2010-01-04' }, - }); - assert.equal(input.onChange.mock.calls[0][0], '2010-01-04'); + it('should call `input.onChange` method when changed', () => { + const onChange = jest.fn(); + const { getByLabelText } = render( + + ); + const input = getByLabelText('resources.bar.fields.foo'); + fireEvent.change(input, { target: { value: '2010-01-04' } }); + expect(onChange.mock.calls[0][0]).toBe('2010-01-04'); }); describe('error message', () => { it('should not be displayed if field is pristine', () => { - const wrapper = shallow( - + const { container } = render( + ); - const DatePicker = wrapper.find('TextField'); - assert.equal(DatePicker.prop('helperText'), ''); + expect(container.querySelector('p')).toBeNull(); }); it('should not be displayed if field has been touched but is valid', () => { - const wrapper = shallow( + const { container } = render( ); - const DatePicker = wrapper.find('TextField'); - assert.equal(DatePicker.prop('helperText'), ''); + expect(container.querySelector('p')).toBeNull(); }); it('should be displayed if field has been touched and is invalid', () => { - const wrapper = shallow( + const { container, queryByText } = render( ); - const DatePicker = wrapper.find('TextField'); - assert.equal(DatePicker.prop('helperText'), 'Required field.'); + expect(container.querySelector('p')).not.toBeNull(); + expect(queryByText('Required field.')).not.toBeNull(); }); }); }); diff --git a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js index 2f50ed67cb7..8ef3368e71b 100644 --- a/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js +++ b/packages/ra-ui-materialui/src/input/RadioButtonGroupInput.spec.js @@ -1,318 +1,203 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; +import { render, cleanup } from 'react-testing-library'; import { RadioButtonGroupInput } from './RadioButtonGroupInput'; describe('', () => { const defaultProps = { + resource: 'bar', source: 'foo', meta: {}, input: {}, translate: x => x, }; - it('should use a mui RadioGroup', () => { - const wrapper = shallow( - - ); - const RadioGroupElement = wrapper.find('RadioGroup'); - assert.equal(RadioGroupElement.length, 1); - }); - - it('should use the input parameter value as the initial input value', () => { - const wrapper = shallow( - - ); - const RadioGroupElement = wrapper.find('RadioGroup').first(); - assert.equal(RadioGroupElement.prop('value'), '2'); - }); + afterEach(cleanup); - it('should use the input parameter value as the selected value', () => { - const wrapper = shallow( - + it('should render choices as radio inputs', () => { + const { getByLabelText, queryByText } = render( + ); - const RadioGroupElement = wrapper.find('RadioGroup').first(); - assert.equal(RadioGroupElement.prop('value'), '2'); + expect(queryByText('hello')).not.toBeNull(); + const input1 = getByLabelText('Male'); + expect(input1.type).toBe('radio'); + expect(input1.name).toBe('foo'); + expect(input1.checked).toBeFalsy(); + const input2 = getByLabelText('Female'); + expect(input2.type).toBe('radio'); + expect(input2.name).toBe('foo'); + expect(input2.checked).toBeFalsy(); }); - it('should render choices as mui FormControlLabel components with a Radio control', () => { - const wrapper = shallow( + it('should use the input parameter value as the initial input value', () => { + const { getByLabelText } = render( ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - assert.equal(RadioButtonElements.length, 2); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); - const RadioButtonElement2 = RadioButtonElements.at(1); - assert.equal(RadioButtonElement2.prop('value'), 'F'); - assert.equal(RadioButtonElement2.prop('label'), 'Female'); + expect(getByLabelText('Male').checked).toBeFalsy(); + expect(getByLabelText('Female').checked).toBeTruthy(); }); it('should use optionValue as value identifier', () => { - const wrapper = shallow( + const { getByLabelText } = render( ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); + expect(getByLabelText('Male').value).toBe('M'); }); it('should use optionValue including "." as value identifier', () => { - const wrapper = shallow( + const { getByLabelText } = render( ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); + expect(getByLabelText('Male').value).toBe('M'); }); it('should use optionText with a string value as text identifier', () => { - const wrapper = shallow( + const { queryByText } = render( ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); + expect(queryByText('Male')).not.toBeNull(); }); it('should use optionText with a string value including "." as text identifier', () => { - const wrapper = shallow( + const { queryByText } = render( ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); + expect(queryByText('Male')).not.toBeNull(); }); it('should use optionText with a function value as text identifier', () => { - const wrapper = shallow( + const { queryByText } = render( choice.foobar} choices={[{ id: 'M', foobar: 'Male' }]} /> ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); + expect(queryByText('Male')).not.toBeNull(); }); it('should use optionText with an element value as text identifier', () => { const Foobar = ({ record }) => {record.foobar}; - const wrapper = shallow( + const { queryByText } = render( } choices={[{ id: 'M', foobar: 'Male' }]} /> ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('value'), 'M'); - assert.deepEqual( - RadioButtonElement1.prop('label'), - - ); + expect(queryByText('Male')).not.toBeNull(); }); it('should translate the choices by default', () => { - const wrapper = shallow( + const { queryByText } = render( `**${x}**`} /> ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('label'), '**Male**'); + expect(queryByText('**Male**')).not.toBeNull(); }); it('should not translate the choices if translateChoice is false', () => { - const wrapper = shallow( + const { queryByText } = render( `**${x}**`} translateChoice={false} /> ); - const RadioButtonElements = wrapper.find( - 'WithStyles(FormControlLabel)' - ); - const RadioButtonElement1 = RadioButtonElements.first(); - assert.equal(RadioButtonElement1.prop('label'), 'Male'); + expect(queryByText('**Male**')).toBeNull(); + expect(queryByText('Male')).not.toBeNull(); }); it('should displayed helperText if prop is present in meta', () => { - const wrapper = shallow( + const { queryByText } = render( `**${x}**`} - translateChoice={false} - meta={{ helperText: 'Can i help you?' }} + meta={{ helperText: 'Can I help you?' }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 1); - assert.equal( - FormHelperTextElement.children().text(), - 'Can i help you?' - ); + expect(queryByText('Can I help you?')).not.toBeNull(); }); describe('error message', () => { it('should not be displayed if field is pristine', () => { - const wrapper = shallow( + const { container } = render( `**${x}**`} - translateChoice={false} meta={{ touched: false }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 0); + expect(container.querySelector('p')).toBeNull(); }); it('should not be displayed if field has been touched but is valid', () => { - const wrapper = shallow( + const { container } = render( `**${x}**`} - translateChoice={false} meta={{ touched: true, error: false }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 0); + expect(container.querySelector('p')).toBeNull(); }); it('should be displayed if field has been touched and is invalid', () => { - const wrapper = shallow( + const { container, queryByText } = render( `**${x}**`} - translateChoice={false} meta={{ touched: true, error: 'Required field.' }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 1); - assert.equal( - FormHelperTextElement.children().text(), - 'Required field.' - ); + expect(container.querySelector('p')).not.toBeNull(); + expect(queryByText('Required field.')).not.toBeNull(); }); it('should display the error and help text if helperText is present', () => { - const wrapper = shallow( + const { queryByText } = render( `**${x}**`} - translateChoice={false} meta={{ touched: true, error: 'Required field.', - helperText: 'Can i help you?', + helperText: 'Can I help you?', }} /> ); - const FormHelperTextElement = wrapper.find( - 'WithStyles(FormHelperText)' - ); - assert.equal(FormHelperTextElement.length, 2); - assert.equal( - FormHelperTextElement.at(0) - .children(0) - .text(), - 'Required field.' - ); - assert.equal( - FormHelperTextElement.at(1) - .children(0) - .text(), - 'Can i help you?' - ); + expect(queryByText('Required field.')).not.toBeNull(); + expect(queryByText('Can I help you?')).not.toBeNull(); }); }); }); diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.js b/packages/ra-ui-materialui/src/input/SelectArrayInput.js index bafa4e1040b..6b8568f83e2 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.js +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.js @@ -53,20 +53,21 @@ const sanitizeRestProps = ({ ...rest }) => rest; -const styles = theme => createStyles({ - root: {}, - chips: { - display: 'flex', - flexWrap: 'wrap', - }, - chip: { - margin: theme.spacing.unit / 4, - }, - select: { - height: 'auto', - overflow: 'auto', - }, -}); +const styles = theme => + createStyles({ + root: {}, + chips: { + display: 'flex', + flexWrap: 'wrap', + }, + chip: { + margin: theme.spacing.unit / 4, + }, + select: { + height: 'auto', + overflow: 'auto', + }, + }); /** * An Input component for a select box allowing multiple selections, using an array of objects for the options @@ -217,7 +218,8 @@ export class SelectArrayInput extends Component { {selected .map(item => choices.find( - choice => choice[optionValue] === item + choice => + get(choice, optionValue) === item ) ) .map(item => ( @@ -229,6 +231,7 @@ export class SelectArrayInput extends Component { ))} )} + data-testid="selectArray" {...options} onChange={this.handleChange} > diff --git a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.js b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.js index dd85f682d16..e1c7338f156 100644 --- a/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.js +++ b/packages/ra-ui-materialui/src/input/SelectArrayInput.spec.js @@ -1,45 +1,47 @@ import React from 'react'; -import assert from 'assert'; -import { shallow } from 'enzyme'; +import expect from 'expect'; +import { render, cleanup } from 'react-testing-library'; + import { SelectArrayInput } from './SelectArrayInput'; describe('', () => { const defaultProps = { classes: {}, + resource: 'bar', source: 'foo', meta: {}, - input: {}, + input: { onChange: () => null, onBlur: () => null }, translate: x => x, }; + afterEach(cleanup); + it('should use a mui Select', () => { - const wrapper = shallow( - + const { queryByTestId } = render( + ); - - const SelectFieldElement = wrapper.find('WithStyles(Select)'); - assert.equal(SelectFieldElement.length, 1); + expect(queryByTestId('selectArray')).not.toBeNull(); }); it('should use the input parameter value as the initial input value', () => { - const wrapper = shallow( + const { getByLabelText } = render( ); - const SelectFieldElement = wrapper.find('WithStyles(Select)'); - assert.deepEqual(SelectFieldElement.prop('value'), [ - 'programming', - 'lifestyle', - ]); + expect(getByLabelText('resources.bar.fields.foo').value).toBe( + 'programming,lifestyle' + ); }); - it('should render choices as mui MenuItem components', () => { - const wrapper = shallow( + it('should reveal choices on click', () => { + const { getByRole, queryByText } = render( ', () => { ]} /> ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - assert.equal(MenuItemElements.length, 3); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'programming'); - assert.equal(MenuItemElement1.childAt(0).text(), 'Programming'); - const MenuItemElement2 = MenuItemElements.at(1); - assert.equal(MenuItemElement2.prop('value'), 'lifestyle'); - assert.equal(MenuItemElement2.childAt(0).text(), 'Lifestyle'); - const MenuItemElement3 = MenuItemElements.at(2); - assert.equal(MenuItemElement3.prop('value'), 'photography'); - assert.equal(MenuItemElement3.childAt(0).text(), 'Photography'); + expect(queryByText('Programming')).toBeNull(); + expect(queryByText('Lifestyle')).toBeNull(); + expect(queryByText('Photography')).toBeNull(); + getByRole('button').click(); + expect(queryByText('Programming')).not.toBeNull(); + expect(queryByText('Lifestyle')).not.toBeNull(); + expect(queryByText('Photography')).not.toBeNull(); }); it('should use optionValue as value identifier', () => { - const wrapper = shallow( + const { getByRole, getByText, getByLabelText } = render( ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).text(), 'Male'); + getByRole('button').click(); + getByText('Male').click(); + expect(getByLabelText('resources.bar.fields.foo').value).toBe('M'); }); it('should use optionValue including "." as value identifier', () => { - const wrapper = shallow( + const { getByRole, getByText, getByLabelText } = render( ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).text(), 'Male'); + getByRole('button').click(); + getByText('Male').click(); + expect(getByLabelText('resources.bar.fields.foo').value).toBe('M'); }); it('should use optionText with a string value as text identifier', () => { - const wrapper = shallow( + const { getByRole, queryByText } = render( ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).text(), 'Male'); + getByRole('button').click(); + expect(queryByText('Male')).not.toBeNull(); }); it('should use optionText with a string value including "." as text identifier', () => { - const wrapper = shallow( + const { getByRole, queryByText } = render( ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).text(), 'Male'); + getByRole('button').click(); + expect(queryByText('Male')).not.toBeNull(); }); it('should use optionText with a function value as text identifier', () => { - const wrapper = shallow( + const { getByRole, queryByText } = render( choice.foobar} choices={[{ id: 'M', foobar: 'Male' }]} /> ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).text(), 'Male'); + getByRole('button').click(); + expect(queryByText('Male')).not.toBeNull(); }); it('should use optionText with an element value as text identifier', () => { const Foobar = ({ record }) => {record.foobar}; - const wrapper = shallow( + const { getByRole, queryByText } = render( } choices={[{ id: 'M', foobar: 'Male' }]} /> ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).type(), Foobar); - assert.deepEqual(MenuItemElement1.childAt(0).prop('record'), { - id: 'M', - foobar: 'Male', - }); + getByRole('button').click(); + expect(queryByText('Male')).not.toBeNull(); }); it('should translate the choices', () => { - const wrapper = shallow( + const { getByRole, queryByText } = render( ', () => { translate={x => `**${x}**`} /> ); - const MenuItemElements = wrapper.find('WithStyles(MenuItem)'); - const MenuItemElement1 = MenuItemElements.first(); - assert.equal(MenuItemElement1.prop('value'), 'M'); - assert.equal(MenuItemElement1.childAt(0).text(), '**Male**'); + getByRole('button').click(); + expect(queryByText('**Male**')).not.toBeNull(); + expect(queryByText('**Female**')).not.toBeNull(); }); it('should displayed helperText if prop is present in meta', () => { - const wrapper = shallow( + const { queryByText } = render( ); - const helperText = wrapper.find('WithStyles(FormHelperText)'); - assert.equal(helperText.length, 1); - assert.equal(helperText.childAt(0).text(), 'Can i help you?'); + expect(queryByText('Can I help you?')).not.toBeNull(); }); describe('error message', () => { it('should not be displayed if field is pristine', () => { - const wrapper = shallow( + const { container } = render( ); - const helperText = wrapper.find('WithStyles(FormHelperText)'); - assert.equal(helperText.length, 0); + expect(container.querySelector('p')).toBeNull(); }); it('should not be displayed if field has been touched but is valid', () => { - const wrapper = shallow( + const { container } = render( ); - const helperText = wrapper.find('WithStyles(FormHelperText)'); - assert.equal(helperText.length, 0); + expect(container.querySelector('p')).toBeNull(); }); it('should be displayed if field has been touched and is invalid', () => { - const wrapper = shallow( + const { container, queryByText } = render( ); - const helperText = wrapper.find('WithStyles(FormHelperText)'); - assert.equal(helperText.length, 1); - assert.equal(helperText.childAt(0).text(), 'Required field.'); + expect(container.querySelector('p')).not.toBeNull(); + expect(queryByText('Required field.')).not.toBeNull(); }); it('should be displayed with an helper Text', () => { - const wrapper = shallow( + const { queryByText } = render( ); - const helperText = wrapper.find('WithStyles(FormHelperText)'); - assert.equal(helperText.length, 2); - assert.equal( - helperText - .at(0) - .childAt(0) - .text(), - 'Required field.' - ); - assert.equal( - helperText - .at(1) - .childAt(0) - .text(), - 'Can i help you?' - ); + expect(queryByText('Required field.')).not.toBeNull(); + expect(queryByText('Can I help you?')).not.toBeNull(); }); }); }); diff --git a/packages/ra-ui-materialui/src/list/Pagination.js b/packages/ra-ui-materialui/src/list/Pagination.js index 4a729adfcef..df9a63d01cc 100644 --- a/packages/ra-ui-materialui/src/list/Pagination.js +++ b/packages/ra-ui-materialui/src/list/Pagination.js @@ -50,6 +50,7 @@ export class Pagination extends Component { render() { const { + width, // used for testing responsive isLoading, page, perPage, @@ -65,6 +66,7 @@ export class Pagination extends Component { return ( ', () => { - it('should display a pagination limit when there is no result', () => { - const wrapper = shallow( - x} - total={0} - changeFormValue={() => true} - changeListParams={() => true} - /> - ); - expect(wrapper.find('pure(translate(PaginationLimit))')).toHaveLength( - 1 - ); - }); + const defaultProps = { + rowsPerPage: 10, + translate: x => x, + }; + + afterEach(cleanup); + + describe('no results mention', () => { + it('should display a pagination limit when there is no result', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.no_results')).not.toBeNull(); + }); + + it('should not display a pagination limit when there are results', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.no_results')).toBeNull(); + }); - it('should not display a pagination limit when there are results', () => { - const wrapper = shallow( - x} - total={1} - ids={[1]} - changeFormValue={() => true} - changeListParams={() => true} - /> - ); - expect(wrapper.find('pure(translate(PaginationLimit))')).toHaveLength( - 0 - ); + it('should not display a pagination limit on an out of bounds page', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.no_results')).toBeNull(); + }); }); - it('should not display a pagination limit on an out of bounds page', () => { - const wrapper = shallow( - x} - total={10} - ids={[]} - page={2} - perPage={10} - changeFormValue={() => true} - changeListParams={() => true} - /> - ); - expect(wrapper.find('pure(translate(PaginationLimit))')).toHaveLength( - 0 - ); + describe('Pagination buttons', () => { + it('should display a next button when there are more results', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.next')).not.toBeNull(); + }); + it('should not display a next button when there are no more results', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.next')).toBeNull(); + }); + it('should display a prev button when there are previous results', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.prev')).not.toBeNull(); + }); + it('should not display a prev button when there are no previous results', () => { + const { queryByText } = render( + + ); + expect(queryByText('ra.navigation.prev')).toBeNull(); + }); }); describe('mobile', () => { - it('should render a without rowsPerPage choice', () => { - const wrapper = shallow( + it('should not render a rowsPerPage choice', () => { + const { queryByText } = render( x} + width="xs" /> - ) - .shallow() - .shallow() - .setProps({ width: 'xs' }) - .shallow() - .shallow(); - const pagination = wrapper.find('WithStyles(TablePagination)'); - expect(pagination.prop('rowsPerPageOptions')).toEqual([]); + ); + expect(queryByText('ra.navigation.page_rows_per_page')).toBeNull(); }); }); + describe('desktop', () => { - it('should render a with rowsPerPage choice', () => { - const wrapper = shallow( + it('should render rowsPerPage choice', () => { + const { queryByText } = render( x} - width={2} + width="md" /> - ) - .shallow() - .shallow() - .shallow() - .shallow(); - const pagination = wrapper.find('WithStyles(TablePagination)'); - expect(pagination.prop('rowsPerPageOptions')).toEqual([5, 10, 25]); + ); + + expect( + queryByText('ra.navigation.page_rows_per_page') + ).not.toBeNull(); }); }); }); diff --git a/yarn.lock b/yarn.lock index 62fed4b7ff7..82a83668481 100644 --- a/yarn.lock +++ b/yarn.lock @@ -799,27 +799,12 @@ dependencies: regenerator-runtime "^0.12.0" -"@babel/runtime@7.0.0-beta.42": - version "7.0.0-beta.42" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.42.tgz#352e40c92e0460d3e82f49bd7e79f6cda76f919f" - integrity sha512-iOGRzUoONLOtmCvjUsZv3mZzgCT6ljHQY5fr1qG1QIiJQwtM7zbPWGGpa3QWETq+UqwWyJnoi5XZDZRwZDFciQ== +"@babel/runtime@^7.0.0": + version "7.4.3" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.3.tgz#79888e452034223ad9609187a0ad1fe0d2ad4bdc" + integrity sha512-9lsJwJLxDh/T3Q3SZszfWOTkk3pHbkmH+3KY+zwIDmsNlxsumuhS2TH3NIpktU4kNvfzy+k3eLT7aTJSPTo0OA== dependencies: - core-js "^2.5.3" - regenerator-runtime "^0.11.1" - -"@babel/runtime@7.0.0-beta.56": - version "7.0.0-beta.56" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.56.tgz#cda612dffd5b1719a7b8e91e3040bd6ae64de8b0" - integrity sha512-vP9XV2VP013UEyZdU9eWClCsm6rQPUYHVNCfmpcv5uKviW7mKmUZq71Y5cr5dYsFKfnGDxSo8h6plUGR60lwHg== - dependencies: - regenerator-runtime "^0.12.0" - -"@babel/runtime@7.0.0-rc.1": - version "7.0.0-rc.1" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-rc.1.tgz#42f36fc5817911c89ea75da2b874054922967616" - integrity sha512-Nifv2kwP/nwR39cAOasNxzjYfpeuf/ZbZNtQz5eYxWTC9yHARU9wItFnAwz1GTZ62MU+AtSjzZPMbLK5Q9hmbg== - dependencies: - regenerator-runtime "^0.12.0" + regenerator-runtime "^0.13.2" "@babel/runtime@^7.2.0": version "7.3.4" @@ -933,46 +918,65 @@ debug "^3.1.0" lodash.once "^4.1.1" -"@material-ui/core@^1.4.0", "@material-ui/core@~1.5.1": - version "1.5.1" - resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-1.5.1.tgz#cb00cb934447ae688e08129f1dab55f54d29d87a" - integrity sha512-hGT0JelWZGZqgWZzRbON/uqFCgWa4XhmEFG/IEd9SBwCU4sWC99Kv1KpywLhYYWecobqT4Dh7ijO1ZaIAk8HyA== +"@material-ui/core@^1.4.0 || ^3.0.0", "@material-ui/core@~3.9.3": + version "3.9.3" + resolved "https://registry.yarnpkg.com/@material-ui/core/-/core-3.9.3.tgz#d378c1f4beb18df9a534ca7258c2c33fb8e0e51f" + integrity sha512-REIj62+zEvTgI/C//YL4fZxrCVIySygmpZglsu/Nl5jPqy3CDjZv1F9ubBYorHqmRgeVPh64EghMMWqk4egmfg== dependencies: - "@babel/runtime" "7.0.0-rc.1" - "@types/jss" "^9.5.3" + "@babel/runtime" "^7.2.0" + "@material-ui/system" "^3.0.0-alpha.0" + "@material-ui/utils" "^3.0.0-alpha.2" + "@types/jss" "^9.5.6" "@types/react-transition-group" "^2.0.8" brcast "^3.0.1" classnames "^2.2.5" csstype "^2.5.2" debounce "^1.1.0" - deepmerge "^2.0.1" + deepmerge "^3.0.0" dom-helpers "^3.2.1" - hoist-non-react-statics "^2.5.0" + hoist-non-react-statics "^3.2.1" is-plain-object "^2.0.4" - jss "^9.3.3" + jss "^9.8.7" jss-camel-case "^6.0.0" jss-default-unit "^8.0.2" jss-global "^3.0.0" jss-nested "^6.0.1" jss-props-sort "^6.0.0" jss-vendor-prefixer "^7.0.0" - keycode "^2.1.9" normalize-scroll-left "^0.1.2" popper.js "^1.14.1" prop-types "^15.6.0" react-event-listener "^0.6.2" - react-jss "^8.1.0" react-transition-group "^2.2.1" - recompose "^0.28.0" + recompose "0.28.0 - 0.30.0" warning "^4.0.1" -"@material-ui/icons@^1.0.0", "@material-ui/icons@~1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-1.1.1.tgz#f104d6a1ac4d3ff34a2bed74b066986b2a7054a5" - integrity sha512-d7I2P1Td4S/1zMAYCIrVQVf+6NUZC5fcIuo0wTrKe/mKxYo9eQ+83lPesI9aBAh+ZTQTjPTqoIvm0WD5e+0uKQ== +"@material-ui/icons@^1.0.0 || ^3.0.0", "@material-ui/icons@~3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@material-ui/icons/-/icons-3.0.2.tgz#d67a6dd1ec8312d3a88ec97944a63daeef24fe10" + integrity sha512-QY/3gJnObZQ3O/e6WjH+0ah2M3MOgLOzCy8HTUoUx9B6dDrS18vP7Ycw3qrDEKlB6q1KNxy6CZHm5FCauWGy2g== + dependencies: + "@babel/runtime" "^7.2.0" + recompose "0.28.0 - 0.30.0" + +"@material-ui/system@^3.0.0-alpha.0": + version "3.0.0-alpha.2" + resolved "https://registry.yarnpkg.com/@material-ui/system/-/system-3.0.0-alpha.2.tgz#096e80c8bb0f70aea435b9e38ea7749ee77b4e46" + integrity sha512-odmxQ0peKpP7RQBQ8koly06YhsPzcoVib1vByVPBH4QhwqBXuYoqlCjt02846fYspAqkrWzjxnWUD311EBbxOA== + dependencies: + "@babel/runtime" "^7.2.0" + deepmerge "^3.0.0" + prop-types "^15.6.0" + warning "^4.0.1" + +"@material-ui/utils@^3.0.0-alpha.2": + version "3.0.0-alpha.3" + resolved "https://registry.yarnpkg.com/@material-ui/utils/-/utils-3.0.0-alpha.3.tgz#836c62ea46f5ffc6f0b5ea05ab814704a86908b1" + integrity sha512-rwMdMZptX0DivkqBuC+Jdq7BYTXwqKai5G5ejPpuEDKpWzi1Oxp+LygGw329FrKpuKeiqpcymlqJTjmy+quWng== dependencies: - "@babel/runtime" "7.0.0-beta.42" - recompose "^0.26.0 || ^0.27.0" + "@babel/runtime" "^7.2.0" + prop-types "^15.6.0" + react-is "^16.6.3" "@mrmlnc/readdir-enhanced@^2.2.1": version "2.2.1" @@ -1090,10 +1094,10 @@ resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.6.tgz#5932ead926307ca21e5b36808257f7c926b06565" integrity sha512-403D4wN95Mtzt2EoQHARf5oe/jEPhzBOBNrunk+ydQGW8WmkQ/E8rViRAEB1qEt/vssfGfNVD6ujP4FVeegrLg== -"@types/jss@^9.5.3": - version "9.5.3" - resolved "https://registry.yarnpkg.com/@types/jss/-/jss-9.5.3.tgz#0c106de3fe0b324cd4173fac7dab26c12cda624e" - integrity sha512-RQWhcpOVyIhGryKpnUyZARwsgmp+tB82O7c75lC4Tjbmr3hPiCnM1wc+pJipVEOsikYXW0IHgeiQzmxQXbnAIA== +"@types/jss@^9.5.6": + version "9.5.8" + resolved "https://registry.yarnpkg.com/@types/jss/-/jss-9.5.8.tgz#258391f42211c042fc965508d505cbdc579baa5b" + integrity sha512-bBbHvjhm42UKki+wZpR89j73ykSXg99/bhuKuYYePtpma3ZAnmeGnl0WxXiZhPGsIfzKwCUkpPC0jlrVMBfRxA== dependencies: csstype "^2.0.0" indefinite-observable "^1.0.1" @@ -4448,11 +4452,6 @@ core-js@^1.0.0: resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY= -core-js@^2.5.3: - version "2.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.5.tgz#44bc8d249e7fb2ff5d00e0341a7ffb94fbf67895" - integrity sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A== - core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -4984,10 +4983,10 @@ deep-is@~0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= -deepmerge@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.1.1.tgz#e862b4e45ea0555072bf51e7fd0d9845170ae768" - integrity sha512-urQxA1smbLZ2cBbXbaYObM1dJ82aJ2H57A1C/Kklfh/ZN1bgH4G/n5KWhdNfOK11W98gqZfyYj7W4frJJRwA2w== +deepmerge@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-3.2.0.tgz#58ef463a57c08d376547f8869fdc5bcee957f44e" + integrity sha512-6+LuZGU7QCNUnAJyX8cIrlzoEgggTM6B7mm+znKOX4t5ltluT9KLjN6g61ECMS0LTsLW7yDpNoxhix5FZcrIow== default-gateway@^2.6.0: version "2.7.2" @@ -7342,6 +7341,13 @@ hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.3.1, hoist-non-react- resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47" integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw== +hoist-non-react-statics@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.0.tgz#b09178f0122184fb95acf525daaecb4d8f45958b" + integrity sha512-0XsbTXxgiaCDYDIWFcwkmerZPSwywfUqYmwT4jzewKTQSWoE6FCMoUVOeBJWK3E/CrWbxRG3m5GzY4lnIwGRBA== + dependencies: + react-is "^16.7.0" + home-or-tmp@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" @@ -8021,11 +8027,6 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= -is-function@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-function/-/is-function-1.0.1.tgz#12cfb98b65b57dd3d193a3121f5f6e2f437602b5" - integrity sha1-Es+5i2W1fdPRk6MSH19uL0N2ArU= - is-generator-fn@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-1.0.0.tgz#969d49e1bb3329f6bb7f09089be26578b2ddd46a" @@ -9050,37 +9051,18 @@ jsprim@^1.2.2: json-schema "0.2.3" verror "1.10.0" -jss-camel-case@^6.0.0, jss-camel-case@^6.1.0: +jss-camel-case@^6.0.0: version "6.1.0" resolved "https://registry.yarnpkg.com/jss-camel-case/-/jss-camel-case-6.1.0.tgz#ccb1ff8d6c701c02a1fed6fb6fb6b7896e11ce44" integrity sha512-HPF2Q7wmNW1t79mCqSeU2vdd/vFFGpkazwvfHMOhPlMgXrJDzdj9viA2SaHk9ZbD5pfL63a8ylp4++irYbbzMQ== dependencies: hyphenate-style-name "^1.0.2" -jss-compose@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/jss-compose/-/jss-compose-5.0.0.tgz#ce01b2e4521d65c37ea42cf49116e5f7ab596484" - integrity sha512-YofRYuiA0+VbeOw0VjgkyO380sA4+TWDrW52nSluD9n+1FWOlDzNbgpZ/Sb3Y46+DcAbOS21W5jo6SAqUEiuwA== - dependencies: - warning "^3.0.0" - jss-default-unit@^8.0.2: version "8.0.2" resolved "https://registry.yarnpkg.com/jss-default-unit/-/jss-default-unit-8.0.2.tgz#cc1e889bae4c0b9419327b314ab1c8e2826890e6" integrity sha512-WxNHrF/18CdoAGw2H0FqOEvJdREXVXLazn7PQYU7V6/BWkCV0GkmWsppNiExdw8dP4TU1ma1dT9zBNJ95feLmg== -jss-expand@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/jss-expand/-/jss-expand-5.3.0.tgz#02be076efe650125c842f5bb6fb68786fe441ed6" - integrity sha512-NiM4TbDVE0ykXSAw6dfFmB1LIqXP/jdd0ZMnlvlGgEMkMt+weJIl8Ynq1DsuBY9WwkNyzWktdqcEW2VN0RAtQg== - -jss-extend@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/jss-extend/-/jss-extend-6.2.0.tgz#4af09d0b72fb98ee229970f8ca852fec1ca2a8dc" - integrity sha512-YszrmcB6o9HOsKPszK7NeDBNNjVyiW864jfoiHoMlgMIg2qlxKw70axZHqgczXHDcoyi/0/ikP1XaHDPRvYtEA== - dependencies: - warning "^3.0.0" - jss-global@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/jss-global/-/jss-global-3.0.0.tgz#e19e5c91ab2b96353c227e30aa2cbd938cdaafa2" @@ -9093,34 +9075,11 @@ jss-nested@^6.0.1: dependencies: warning "^3.0.0" -jss-preset-default@^4.3.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/jss-preset-default/-/jss-preset-default-4.5.0.tgz#d3a457012ccd7a551312014e394c23c4b301cadd" - integrity sha512-qZbpRVtHT7hBPpZEBPFfafZKWmq3tA/An5RNqywDsZQGrlinIF/mGD9lmj6jGqu8GrED2SMHZ3pPKLmjCZoiaQ== - dependencies: - jss-camel-case "^6.1.0" - jss-compose "^5.0.0" - jss-default-unit "^8.0.2" - jss-expand "^5.3.0" - jss-extend "^6.2.0" - jss-global "^3.0.0" - jss-nested "^6.0.1" - jss-props-sort "^6.0.0" - jss-template "^1.0.1" - jss-vendor-prefixer "^7.0.0" - jss-props-sort@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/jss-props-sort/-/jss-props-sort-6.0.0.tgz#9105101a3b5071fab61e2d85ea74cc22e9b16323" integrity sha512-E89UDcrphmI0LzmvYk25Hp4aE5ZBsXqMWlkFXS0EtPkunJkRr+WXdCNYbXbksIPnKlBenGB9OxzQY+mVc70S+g== -jss-template@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/jss-template/-/jss-template-1.0.1.tgz#09aed9d86cc547b07f53ef355d7e1777f7da430a" - integrity sha512-m5BqEWha17fmIVXm1z8xbJhY6GFJxNB9H68GVnCWPyGYfxiAgY9WTQyvDAVj+pYRgrXSOfN5V1T4+SzN1sJTeg== - dependencies: - warning "^3.0.0" - jss-vendor-prefixer@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/jss-vendor-prefixer/-/jss-vendor-prefixer-7.0.0.tgz#0166729650015ef19d9f02437c73667231605c71" @@ -9128,7 +9087,7 @@ jss-vendor-prefixer@^7.0.0: dependencies: css-vendor "^0.3.8" -jss@^9.3.3, jss@^9.7.0: +jss@^9.8.7: version "9.8.7" resolved "https://registry.yarnpkg.com/jss/-/jss-9.8.7.tgz#ed9763fc0f2f0260fc8260dac657af61e622ce05" integrity sha512-awj3XRZYxbrmmrx9LUSj5pXSUfm12m8xzi/VKeqI1ZwWBtQ0kVPTs3vYs32t4rFw83CgFDukA8wKzOE9sMQnoQ== @@ -9144,11 +9103,6 @@ jsx-ast-utils@^2.0.1: dependencies: array-includes "^3.0.3" -keycode@^2.1.9: - version "2.2.0" - resolved "https://registry.yarnpkg.com/keycode/-/keycode-2.2.0.tgz#3d0af56dc7b8b8e5cba8d0a97f107204eec22b04" - integrity sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ= - keyv@3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/keyv/-/keyv-3.0.0.tgz#44923ba39e68b12a7cec7df6c3268c031f2ef373" @@ -12251,16 +12205,10 @@ react-is@^16.6.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.7.0.tgz#c1bd21c64f1f1364c6f70695ec02d69392f41bfa" integrity sha512-Z0VRQdF4NPDoI0tsXVMLkJLiwEBa+RP66g0xDHxgxysxSoCUccSten4RTF/UFvZF1dZvZ9Zu1sx+MDXwcOR34g== -react-jss@^8.1.0: - version "8.5.1" - resolved "https://registry.yarnpkg.com/react-jss/-/react-jss-8.5.1.tgz#f97c72f6a1c86aa6408932a2a2836ce40c0ab9fc" - integrity sha512-5R3qCdGkE+K0+B4tuRyx8idLV7q2pT1QbGomGqberCQ/xLKEQbDukH7ER2QLkpIYqtRkeciG9S03uDJwC1o2gw== - dependencies: - hoist-non-react-statics "^2.5.0" - jss "^9.7.0" - jss-preset-default "^4.3.0" - prop-types "^15.6.0" - theming "^1.3.0" +react-is@^16.6.3, react-is@^16.7.0: + version "16.8.6" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" + integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4: version "3.0.4" @@ -12586,24 +12534,24 @@ rechoir@^0.6.2: dependencies: resolve "^1.1.6" -"recompose@^0.26.0 || ^0.27.0", recompose@^0.27.1: - version "0.27.1" - resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba" - integrity sha512-p7xsyi/rfNjHfdP7vPU02uSFa+Q1eHhjKrvO+3+kRP4Ortj+MxEmpmd+UQtBGM2D2iNAjzNI5rCyBKp9Ob5McA== +"recompose@0.28.0 - 0.30.0": + version "0.30.0" + resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0" + integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w== dependencies: - babel-runtime "^6.26.0" + "@babel/runtime" "^7.0.0" change-emitter "^0.1.2" fbjs "^0.8.1" hoist-non-react-statics "^2.3.1" react-lifecycles-compat "^3.0.2" symbol-observable "^1.0.4" -recompose@^0.28.0: - version "0.28.2" - resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.28.2.tgz#19e679227bdf979e0d31b73ffe7ae38c9194f4a7" - integrity sha512-baVNKQBQAAAuLRnv6Cb/6/j59a1BVj6c6Pags1KXVyRB0yPfQVUZtuAUnqHDBXoR8iXPrLGWE4RNtCQ/AaRP3g== +recompose@^0.27.1: + version "0.27.1" + resolved "https://registry.yarnpkg.com/recompose/-/recompose-0.27.1.tgz#1a49e931f183634516633bbb4f4edbfd3f38a7ba" + integrity sha512-p7xsyi/rfNjHfdP7vPU02uSFa+Q1eHhjKrvO+3+kRP4Ortj+MxEmpmd+UQtBGM2D2iNAjzNI5rCyBKp9Ob5McA== dependencies: - "@babel/runtime" "7.0.0-beta.56" + babel-runtime "^6.26.0" change-emitter "^0.1.2" fbjs "^0.8.1" hoist-non-react-statics "^2.3.1" @@ -12718,6 +12666,11 @@ regenerator-runtime@^0.12.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de" integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg== +regenerator-runtime@^0.13.2: + version "0.13.2" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz#32e59c9a6fb9b1a4aff09b4930ca2d4477343447" + integrity sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA== + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" @@ -14232,16 +14185,6 @@ textextensions@2: resolved "https://registry.yarnpkg.com/textextensions/-/textextensions-2.2.0.tgz#38ac676151285b658654581987a0ce1a4490d286" integrity sha512-j5EMxnryTvKxwH2Cq+Pb43tsf6sdEgw6Pdwxk83mPaq0ToeFJt6WE4J3s5BqY7vmjlLgkgXvhtXUxo80FyBhCA== -theming@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/theming/-/theming-1.3.0.tgz#286d5bae80be890d0adc645e5ca0498723725bdc" - integrity sha512-ya5Ef7XDGbTPBv5ENTwrwkPUexrlPeiAg/EI9kdlUAZhNlRbCdhMKRgjNX1IcmsmiPcqDQZE6BpSaH+cr31FKw== - dependencies: - brcast "^3.0.1" - is-function "^1.0.1" - is-plain-object "^2.0.1" - prop-types "^15.5.8" - throat@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/throat/-/throat-4.1.0.tgz#89037cbc92c56ab18926e6ba4cbb200e15672a6a"