From aeb8acdddce0aa8b62e6158d7cf66f41f2336e5f Mon Sep 17 00:00:00 2001 From: Tiago Schenkel Date: Thu, 7 Feb 2019 16:17:41 +0100 Subject: [PATCH 01/35] Ensure that the inputValue is an array and fix #2361 --- .../src/input/AutocompleteArrayInput.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js index 613c6fe6c2f..699a03d530d 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js @@ -104,25 +104,27 @@ const styles = theme => ({ export class AutocompleteArrayInput extends React.Component { state = { dirty: false, - inputValue: null, + inputValue: [], searchText: '', suggestions: [], }; inputEl = null; + getInputValue = inputValue => (inputValue === '' ? [] : inputValue); + componentWillMount() { this.setState({ - inputValue: this.props.input.value, + inputValue: this.getInputValue(this.props.input.value), suggestions: this.props.choices, }); } componentWillReceiveProps(nextProps) { const { choices, input, inputValueMatcher } = nextProps; - if (!isEqual(input.value, this.state.inputValue)) { + if (!isEqual(this.getInputValue(input.value), this.state.inputValue)) { this.setState({ - inputValue: input.value, + inputValue: this.getInputValue(input.value), dirty: false, suggestions: this.props.choices, }); @@ -245,7 +247,7 @@ export class AutocompleteArrayInput extends React.Component { onUpdateInput={onChange} onAdd={this.handleAdd} onDelete={this.handleDelete} - value={input.value} + value={this.getInputValue(input.value)} inputRef={storeInputRef} error={touched && error} helperText={touched && error && helperText} From 3e58c94f03fbd10857a6819521a05079d4b71e70 Mon Sep 17 00:00:00 2001 From: Tiago Schenkel Date: Thu, 7 Feb 2019 16:54:58 +0100 Subject: [PATCH 02/35] Force the Popper component to reposition the popup when the input is moved and fix #2259 --- .../src/input/AutocompleteArrayInput.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js index 613c6fe6c2f..c1ff2e1eb73 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js @@ -335,11 +335,19 @@ export class AutocompleteArrayInput extends React.Component { children, } = options; + // Force the Popper component to reposition the popup when this.inputEl is moved to another location + const anchorEl = !this.inputEl + ? null + : { + getBoundingClientRect: () => + this.inputEl.getBoundingClientRect(), + }; + return ( From 33fafcf1e3f2c9bb681849e10185b09977210a9c Mon Sep 17 00:00:00 2001 From: Tiago Schenkel Date: Fri, 8 Feb 2019 14:11:37 +0100 Subject: [PATCH 03/35] Avoid to redraw Popper component when inputEl is not moving --- .../src/input/AutocompleteArrayInput.js | 35 ++++++++++++++----- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js index c1ff2e1eb73..fb7f3f7f772 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js @@ -110,6 +110,7 @@ export class AutocompleteArrayInput extends React.Component { }; inputEl = null; + anchorEl = null; componentWillMount() { this.setState({ @@ -236,6 +237,7 @@ export class AutocompleteArrayInput extends React.Component { // but Autosuggest also needs this reference (it provides the ref prop) const storeInputRef = input => { this.inputEl = input; + this.updateAnchorEl(); ref(input); }; @@ -329,25 +331,42 @@ export class AutocompleteArrayInput extends React.Component { input.onChange(this.state.inputValue.filter(value => value !== chip)); }; + updateAnchorEl() { + if (!this.inputEl) { + return; + } + + if (!this.anchorEl) { + const inputPosition = this.inputEl.getBoundingClientRect(); + + this.anchorEl = { getBoundingClientRect: () => inputPosition }; + } else { + const anchorPosition = this.anchorEl.getBoundingClientRect(); + const inputPosition = this.inputEl.getBoundingClientRect(); + + if ( + anchorPosition.x !== inputPosition.x || + anchorPosition.y !== inputPosition.y + ) { + this.anchorEl = { getBoundingClientRect: () => inputPosition }; + } + } + } + renderSuggestionsContainer = options => { const { containerProps: { className, ...containerProps }, children, } = options; - // Force the Popper component to reposition the popup when this.inputEl is moved to another location - const anchorEl = !this.inputEl - ? null - : { - getBoundingClientRect: () => - this.inputEl.getBoundingClientRect(), - }; + // Force the Popper component to reposition the popup only when this.inputEl is moved to another location + this.updateAnchorEl(); return ( From f0a7e916ad1aa9fd55a8ae3a3bd3c42bb4f3b84b Mon Sep 17 00:00:00 2001 From: Tiago Schenkel Date: Fri, 8 Feb 2019 14:55:54 +0100 Subject: [PATCH 04/35] Simplify internal logic of updateAnchorEl method --- .../ra-ui-materialui/src/input/AutocompleteArrayInput.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js index fb7f3f7f772..8b20969bd7c 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js @@ -336,13 +336,12 @@ export class AutocompleteArrayInput extends React.Component { return; } - if (!this.anchorEl) { - const inputPosition = this.inputEl.getBoundingClientRect(); + const inputPosition = this.inputEl.getBoundingClientRect(); + if (!this.anchorEl) { this.anchorEl = { getBoundingClientRect: () => inputPosition }; } else { const anchorPosition = this.anchorEl.getBoundingClientRect(); - const inputPosition = this.inputEl.getBoundingClientRect(); if ( anchorPosition.x !== inputPosition.x || From 82b134a5a5b7303e211d696195360636e31cb07d Mon Sep 17 00:00:00 2001 From: Giovanni Carlo Marasco Date: Tue, 12 Feb 2019 11:53:26 -0600 Subject: [PATCH 05/35] Update UnitTesting.md The previous import location threw an error for me as it couldn't be found. --- docs/UnitTesting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/UnitTesting.md b/docs/UnitTesting.md index 62faeea8eeb..dd8d96e6fa4 100644 --- a/docs/UnitTesting.md +++ b/docs/UnitTesting.md @@ -21,7 +21,7 @@ Luckily, react-admin provides access to a `TestContext` wrapper component that c ```jsx import React from 'react'; -import { TestContext } from 'ra-core'; +import { TestContext } from 'react-admin'; import { mount } from 'enzyme'; import MyCustomEditView from './my-custom-edit-view'; From 429c4b606cacc2d3d85a8b1d61e1f8c5e99f31b1 Mon Sep 17 00:00:00 2001 From: Florian Mounier Date: Wed, 13 Feb 2019 10:24:04 +0100 Subject: [PATCH 06/35] Fix bad GET_MANY query for ra-data-json-server using a multiple id filter instead of a greedy regular expression. --- packages/ra-data-json-server/src/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-data-json-server/src/index.js b/packages/ra-data-json-server/src/index.js index 1d8fc904cdc..4316c2a1dad 100644 --- a/packages/ra-data-json-server/src/index.js +++ b/packages/ra-data-json-server/src/index.js @@ -81,7 +81,7 @@ export default (apiUrl, httpClient = fetchUtils.fetchJson) => { break; case GET_MANY: { const query = { - [`id_like`]: params.ids.join('|'), + id: params.ids }; url = `${apiUrl}/${resource}?${stringify(query)}`; break; From 8e2fba31b5661f3813bbf7073dabad753f42d4b3 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 10:56:47 +0100 Subject: [PATCH 07/35] [RFR] Finalize i18n Migration to TypeScript --- .../src/i18n/{translate.spec.js => translate.spec.tsx} | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) rename packages/ra-core/src/i18n/{translate.spec.js => translate.spec.tsx} (59%) diff --git a/packages/ra-core/src/i18n/translate.spec.js b/packages/ra-core/src/i18n/translate.spec.tsx similarity index 59% rename from packages/ra-core/src/i18n/translate.spec.js rename to packages/ra-core/src/i18n/translate.spec.tsx index c0e12cc7899..ae193e21c77 100644 --- a/packages/ra-core/src/i18n/translate.spec.js +++ b/packages/ra-core/src/i18n/translate.spec.tsx @@ -1,10 +1,14 @@ -import React from 'react'; +import React, { SFC } from 'react'; import translate from './translate'; +import { TranslationContextProps } from './TranslationContext'; +interface Props extends TranslationContextProps { + foo: string; +} describe('translate HOC', () => { it('should conserve base component default props', () => { - const Component = () =>
; + const Component: SFC = () =>
; Component.defaultProps = { foo: 'bar' }; const TranslatedComponent = translate(Component); From 87a568160029348f17b4c8e7dc4cf38e30908ce2 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 12:10:56 +0100 Subject: [PATCH 08/35] [RFR] Migrate ra-core form code to TypeScript --- ...umer.spec.js => FormDataConsumer.spec.tsx} | 7 +- ...rmDataConsumer.js => FormDataConsumer.tsx} | 35 +++++-- .../{FormField.spec.js => FormField.spec.tsx} | 0 .../src/form/{FormField.js => FormField.tsx} | 21 +++- .../src/form/{addField.js => addField.tsx} | 9 +- .../src/form/{constants.js => constants.ts} | 0 ...dleware.spec.js => formMiddleware.spec.ts} | 2 +- .../{formMiddleware.js => formMiddleware.ts} | 2 +- ...alues.spec.js => getDefaultValues.spec.ts} | 0 ...etDefaultValues.js => getDefaultValues.ts} | 0 .../ra-core/src/form/{index.js => index.ts} | 0 packages/ra-core/src/form/types.ts | 5 + .../{validate.spec.js => validate.spec.ts} | 0 .../src/form/{validate.js => validate.ts} | 96 ++++++++++++++----- ...alue.spec.js => withDefaultValue.spec.tsx} | 8 +- ...thDefaultValue.js => withDefaultValue.tsx} | 16 +++- 16 files changed, 152 insertions(+), 49 deletions(-) rename packages/ra-core/src/form/{FormDataConsumer.spec.js => FormDataConsumer.spec.tsx} (88%) rename packages/ra-core/src/form/{FormDataConsumer.js => FormDataConsumer.tsx} (82%) rename packages/ra-core/src/form/{FormField.spec.js => FormField.spec.tsx} (100%) rename packages/ra-core/src/form/{FormField.js => FormField.tsx} (61%) rename packages/ra-core/src/form/{addField.js => addField.tsx} (54%) rename packages/ra-core/src/form/{constants.js => constants.ts} (100%) rename packages/ra-core/src/form/{formMiddleware.spec.js => formMiddleware.spec.ts} (97%) rename packages/ra-core/src/form/{formMiddleware.js => formMiddleware.ts} (96%) rename packages/ra-core/src/form/{getDefaultValues.spec.js => getDefaultValues.spec.ts} (100%) rename packages/ra-core/src/form/{getDefaultValues.js => getDefaultValues.ts} (100%) rename packages/ra-core/src/form/{index.js => index.ts} (100%) create mode 100644 packages/ra-core/src/form/types.ts rename packages/ra-core/src/form/{validate.spec.js => validate.spec.ts} (100%) rename packages/ra-core/src/form/{validate.js => validate.ts} (70%) rename packages/ra-core/src/form/{withDefaultValue.spec.js => withDefaultValue.spec.tsx} (92%) rename packages/ra-core/src/form/{withDefaultValue.js => withDefaultValue.tsx} (79%) diff --git a/packages/ra-core/src/form/FormDataConsumer.spec.js b/packages/ra-core/src/form/FormDataConsumer.spec.tsx similarity index 88% rename from packages/ra-core/src/form/FormDataConsumer.spec.js rename to packages/ra-core/src/form/FormDataConsumer.spec.tsx index 136f1ac7be9..798f19b97ed 100644 --- a/packages/ra-core/src/form/FormDataConsumer.spec.js +++ b/packages/ra-core/src/form/FormDataConsumer.spec.tsx @@ -9,7 +9,11 @@ describe('FormDataConsumerView', () => { const formData = { id: 123, title: 'A title' }; shallow( - + {children} ); @@ -27,6 +31,7 @@ describe('FormDataConsumerView', () => { shallow( string; +} + +interface Props extends ConnectedProps { + formData: any; + index?: number; +} /** * Get the current (edited) value of the record from the form and pass it @@ -44,7 +56,7 @@ import warning from '../util/warning'; * * ); */ -export const FormDataConsumerView = ({ +export const FormDataConsumerView: SFC = ({ children, form, formData, @@ -60,7 +72,7 @@ export const FormDataConsumerView = ({ // If we have an index, we are in an iterator like component (such as the SimpleFormIterator) if (typeof index !== 'undefined') { scopedFormData = get(formData, source); - getSource = scopedSource => { + getSource = (scopedSource: string) => { getSourceHasBeenCalled = true; return `${source}.${scopedSource}`; }; @@ -100,12 +112,17 @@ export const FormDataConsumerView = ({ return ret === undefined ? null : ret; }; -FormDataConsumerView.propTypes = { - children: PropTypes.func.isRequired, - data: PropTypes.object, -}; +interface ConnectedProps { + children: (params: ChildrenFunctionParams) => ReactNode; + form: string; + record?: any; + source: string; +} -const mapStateToProps = (state, { form, record }) => ({ +const mapStateToProps = ( + state: ReduxState, + { form, record }: ConnectedProps +) => ({ formData: getFormValues(form)(state) || record, }); @@ -113,7 +130,7 @@ const ConnectedFormDataConsumerView = connect(mapStateToProps)( FormDataConsumerView ); -const FormDataConsumer = props => ( +const FormDataConsumer = (props: ConnectedProps) => ( {({ form }) => } diff --git a/packages/ra-core/src/form/FormField.spec.js b/packages/ra-core/src/form/FormField.spec.tsx similarity index 100% rename from packages/ra-core/src/form/FormField.spec.js rename to packages/ra-core/src/form/FormField.spec.tsx diff --git a/packages/ra-core/src/form/FormField.js b/packages/ra-core/src/form/FormField.tsx similarity index 61% rename from packages/ra-core/src/form/FormField.js rename to packages/ra-core/src/form/FormField.tsx index 7bbb29947e1..902d11c0312 100644 --- a/packages/ra-core/src/form/FormField.js +++ b/packages/ra-core/src/form/FormField.tsx @@ -1,17 +1,29 @@ -import React from 'react'; +import React, { ComponentType, SFC } from 'react'; import PropTypes from 'prop-types'; import { Field } from 'redux-form'; import withDefaultValue from './withDefaultValue'; +import { Validator } from './validate'; +import { InputProps } from './types'; export const isRequired = validate => { - if (validate && validate.isRequired) return true; + if (validate && validate.isRequired) { + return true; + } if (Array.isArray(validate)) { return !!validate.find(it => it.isRequired); } return false; }; -export const FormFieldView = ({ input, ...props }) => +interface Props { + component: ComponentType; + defaultValue: any; + input?: any; + source: string; + validate: Validator | Validator[]; +} + +export const FormFieldView: SFC = ({ input, ...props }) => input ? ( // An ancestor is already decorated by Field React.createElement(props.component, { input, ...props }) ) : ( @@ -30,4 +42,5 @@ FormFieldView.propTypes = { validate: PropTypes.oneOfType([PropTypes.func, PropTypes.array]), }; -export default withDefaultValue(FormFieldView); +const FormField = withDefaultValue(FormFieldView); +export default FormField; diff --git a/packages/ra-core/src/form/addField.js b/packages/ra-core/src/form/addField.tsx similarity index 54% rename from packages/ra-core/src/form/addField.js rename to packages/ra-core/src/form/addField.tsx index a88cd82a1b8..b9acc4d9459 100644 --- a/packages/ra-core/src/form/addField.js +++ b/packages/ra-core/src/form/addField.tsx @@ -1,7 +1,12 @@ -import React from 'react'; +import React, { ReactType } from 'react'; import FormField from './FormField'; -export default (BaseComponent, fieldProps = {}) => { +export default ( + BaseComponent: ReactType, + fieldProps: { + [key: string]: any; + } = {} +) => { const WithFormField = props => ( ); diff --git a/packages/ra-core/src/form/constants.js b/packages/ra-core/src/form/constants.ts similarity index 100% rename from packages/ra-core/src/form/constants.js rename to packages/ra-core/src/form/constants.ts diff --git a/packages/ra-core/src/form/formMiddleware.spec.js b/packages/ra-core/src/form/formMiddleware.spec.ts similarity index 97% rename from packages/ra-core/src/form/formMiddleware.spec.js rename to packages/ra-core/src/form/formMiddleware.spec.ts index 6f54cd441fa..176f4dbfa0a 100644 --- a/packages/ra-core/src/form/formMiddleware.spec.js +++ b/packages/ra-core/src/form/formMiddleware.spec.ts @@ -2,7 +2,7 @@ import { LOCATION_CHANGE } from 'react-router-redux'; import { destroy } from 'redux-form'; import formMiddleware from './formMiddleware'; -import { REDUX_FORM_NAME } from '../form/constants'; +import { REDUX_FORM_NAME } from './constants'; import { resetForm } from '../actions/formActions'; describe('form middleware', () => { diff --git a/packages/ra-core/src/form/formMiddleware.js b/packages/ra-core/src/form/formMiddleware.ts similarity index 96% rename from packages/ra-core/src/form/formMiddleware.js rename to packages/ra-core/src/form/formMiddleware.ts index 24cfaf862cb..372fced2823 100644 --- a/packages/ra-core/src/form/formMiddleware.js +++ b/packages/ra-core/src/form/formMiddleware.ts @@ -3,7 +3,7 @@ import { destroy } from 'redux-form'; import isEqual from 'lodash/isEqual'; import { resetForm } from '../actions/formActions'; -import { REDUX_FORM_NAME } from '../form/constants'; +import { REDUX_FORM_NAME } from './constants'; /** * This middleware ensure that whenever a location change happen, we get the diff --git a/packages/ra-core/src/form/getDefaultValues.spec.js b/packages/ra-core/src/form/getDefaultValues.spec.ts similarity index 100% rename from packages/ra-core/src/form/getDefaultValues.spec.js rename to packages/ra-core/src/form/getDefaultValues.spec.ts diff --git a/packages/ra-core/src/form/getDefaultValues.js b/packages/ra-core/src/form/getDefaultValues.ts similarity index 100% rename from packages/ra-core/src/form/getDefaultValues.js rename to packages/ra-core/src/form/getDefaultValues.ts diff --git a/packages/ra-core/src/form/index.js b/packages/ra-core/src/form/index.ts similarity index 100% rename from packages/ra-core/src/form/index.js rename to packages/ra-core/src/form/index.ts diff --git a/packages/ra-core/src/form/types.ts b/packages/ra-core/src/form/types.ts new file mode 100644 index 00000000000..64b3913bd63 --- /dev/null +++ b/packages/ra-core/src/form/types.ts @@ -0,0 +1,5 @@ +export interface InputProps { + defaultValue?: any; + input?: any; + source: string; +} diff --git a/packages/ra-core/src/form/validate.spec.js b/packages/ra-core/src/form/validate.spec.ts similarity index 100% rename from packages/ra-core/src/form/validate.spec.js rename to packages/ra-core/src/form/validate.spec.ts diff --git a/packages/ra-core/src/form/validate.js b/packages/ra-core/src/form/validate.ts similarity index 70% rename from packages/ra-core/src/form/validate.js rename to packages/ra-core/src/form/validate.ts index 5ef13da8693..e7d4c854edb 100644 --- a/packages/ra-core/src/form/validate.js +++ b/packages/ra-core/src/form/validate.ts @@ -1,13 +1,38 @@ import lodashMemoize from 'lodash/memoize'; +import { Translate } from '../types'; /* eslint-disable no-underscore-dangle */ /* @link http://stackoverflow.com/questions/46155/validate-email-address-in-javascript */ const EMAIL_REGEX = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; // eslint-disable-line no-useless-escape -const isEmpty = value => +const isEmpty = (value: any) => typeof value === 'undefined' || value === null || value === ''; -const getMessage = (message, messageArgs, value, values, props) => +export type Validator = ( + value: any, + values: any, + props: any +) => string | null | undefined; + +interface MessageFuncParams { + args: any; + value: any; + values: any; + translate: Translate; + [key: string]: any; +} + +type MessageFunc = (params: MessageFuncParams) => string; + +const getMessage = ( + message: string | MessageFunc, + messageArgs: any, + value: any, + values: any, + props: { + translate: Translate; + } +) => typeof message === 'function' ? message({ args: messageArgs, @@ -23,7 +48,10 @@ const getMessage = (message, messageArgs, value, values, props) => // If we define validation functions directly in JSX, it will // result in a new function at every render, and then trigger infinite re-render. // Hence, we memoize every built-in validator to prevent a "Maximum call stack" error. -const memoize = fn => lodashMemoize(fn, (...args) => JSON.stringify(args)); +const memoize = (fn: any) => + lodashMemoize(fn, (...args) => JSON.stringify(args)); + +type Required = (message?: string | MessageFunc) => Validator; /** * Required validator @@ -37,16 +65,19 @@ const memoize = fn => lodashMemoize(fn, (...args) => JSON.stringify(args)); * const titleValidators = [required('The title is required')]; * */ -export const required = memoize((message = 'ra.validation.required') => - Object.assign( - (value, values, props) => - isEmpty(value) - ? getMessage(message, undefined, value, values, props) - : undefined, - { isRequired: true } - ) +export const required: Required = memoize( + (message = 'ra.validation.required') => + Object.assign( + (value, values, props) => + isEmpty(value) + ? getMessage(message, undefined, value, values, props) + : undefined, + { isRequired: true } + ) ); +type MinLength = (min: number, message?: string | MessageFunc) => Validator; + /** * Minimum length validator * @@ -60,13 +91,15 @@ export const required = memoize((message = 'ra.validation.required') => * const passwordValidators = [minLength(10, 'Should be at least 10 characters')]; * */ -export const minLength = memoize( +export const minLength: MinLength = memoize( (min, message = 'ra.validation.minLength') => (value, values, props) => !isEmpty(value) && value.length < min ? getMessage(message, { min }, value, values, props) : undefined ); +type MaxLength = (max: number, message?: string | MessageFunc) => Validator; + /** * Maximum length validator * @@ -80,13 +113,15 @@ export const minLength = memoize( * const nameValidators = [maxLength(10, 'Should be at most 10 characters')]; * */ -export const maxLength = memoize( +export const maxLength: MaxLength = memoize( (max, message = 'ra.validation.maxLength') => (value, values, props) => !isEmpty(value) && value.length > max ? getMessage(message, { max }, value, values, props) : undefined ); +type MinValue = (min: number, message?: string | MessageFunc) => Validator; + /** * Minimum validator * @@ -100,13 +135,15 @@ export const maxLength = memoize( * const fooValidators = [minValue(5, 'Should be more than 5')]; * */ -export const minValue = memoize( +export const minValue: MinValue = memoize( (min, message = 'ra.validation.minValue') => (value, values, props) => !isEmpty(value) && value < min ? getMessage(message, { min }, value, values, props) : undefined ); +type MaxValue = (max: number, message?: string | MessageFunc) => Validator; + /** * Maximum validator * @@ -120,13 +157,15 @@ export const minValue = memoize( * const fooValidators = [maxValue(10, 'Should be less than 10')]; * */ -export const maxValue = memoize( +export const maxValue: MaxValue = memoize( (max, message = 'ra.validation.maxValue') => (value, values, props) => !isEmpty(value) && value > max ? getMessage(message, { max }, value, values, props) : undefined ); +type NumberValidator = (message?: string | MessageFunc) => Validator; + /** * Number validator * @@ -140,17 +179,22 @@ export const maxValue = memoize( * */ // tslint:disable-next-line:variable-name -export const number = memoize( +export const number: NumberValidator = memoize( (message = 'ra.validation.number') => (value, values, props) => !isEmpty(value) && isNaN(Number(value)) ? getMessage(message, undefined, value, values, props) : undefined ); +type RegedValidator = ( + pattern: RegExp, + message?: string | MessageFunc +) => Validator; + /** * Regular expression validator * - * Returns an error if the value does not mactch the pattern given as parameter + * Returns an error if the value does not match the pattern given as parameter * * @param {RegExp} pattern * @param {string|function} message @@ -160,7 +204,7 @@ export const number = memoize( * const zipValidators = [regex(/^\d{5}(?:[-\s]\d{4})?$/, 'Must be a zip code')]; * */ -export const regex = lodashMemoize( +export const regex: RegedValidator = lodashMemoize( (pattern, message = 'ra.validation.regex') => (value, values, props) => !isEmpty(value) && typeof value === 'string' && !pattern.test(value) ? getMessage(message, { pattern }, value, values, props) @@ -170,6 +214,8 @@ export const regex = lodashMemoize( } ); +type EmailValidator = (message?: string | MessageFunc) => Validator; + /** * Email validator * @@ -182,15 +228,19 @@ export const regex = lodashMemoize( * const emailValidators = [email('Must be an email')]; * */ -export const email = memoize((message = 'ra.validation.email') => - regex(EMAIL_REGEX, message) +export const email: EmailValidator = memoize( + (message = 'ra.validation.email') => regex(EMAIL_REGEX, message) ); -const oneOfTypeMessage = ({ list }, value, values, { translate }) => { +const oneOfTypeMessage: MessageFunc = ({ list, value, values, translate }) => translate('ra.validation.oneOf', { options: list.join(', '), }); -}; + +type ChoicesValidator = ( + list: any[], + message?: string | MessageFunc +) => Validator; /** * Choices validator @@ -205,7 +255,7 @@ const oneOfTypeMessage = ({ list }, value, values, { translate }) => { * const genderValidators = [choices(['male', 'female'], 'Must be either Male or Female')]; * */ -export const choices = memoize( +export const choices: ChoicesValidator = memoize( (list, message = oneOfTypeMessage) => (value, values, props) => !isEmpty(value) && list.indexOf(value) === -1 ? getMessage(message, { list }, value, values, props) diff --git a/packages/ra-core/src/form/withDefaultValue.spec.js b/packages/ra-core/src/form/withDefaultValue.spec.tsx similarity index 92% rename from packages/ra-core/src/form/withDefaultValue.spec.js rename to packages/ra-core/src/form/withDefaultValue.spec.tsx index 79e41b6fdab..bd474523cdc 100644 --- a/packages/ra-core/src/form/withDefaultValue.spec.js +++ b/packages/ra-core/src/form/withDefaultValue.spec.tsx @@ -1,7 +1,7 @@ import assert from 'assert'; import { shallow } from 'enzyme'; import React from 'react'; -import { DefaultValue } from './withDefaultValue'; +import { DefaultValueView } from './withDefaultValue'; describe('withDefaultValue', () => { describe('', () => { @@ -10,7 +10,7 @@ describe('withDefaultValue', () => { it('should not initialize the form if no default value', () => { const initializeForm = jest.fn(); shallow( - { it('should initialize the form with default value on mount', () => { const initializeForm = jest.fn(); shallow( - { it('should call initializeForm if a defaultValue changes', () => { const initializeForm = jest.fn(); const wrapper = shallow( - ; + initializeForm: typeof initializeFormAction; +} + +export class DefaultValueView extends Component { static propTypes = { decoratedComponent: PropTypes.oneOfType([ PropTypes.element, @@ -52,8 +58,10 @@ export class DefaultValue extends Component { } } -export default DecoratedComponent => +const DefaultValue = (DecoratedComponent: ComponentType) => connect( () => ({ decoratedComponent: DecoratedComponent }), { initializeForm: initializeFormAction } - )(DefaultValue); + )(DefaultValueView); + +export default DefaultValue; From 5d953e60e59ebd3aaa1328fdc9453bc4ff03df45 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 12:44:47 +0100 Subject: [PATCH 09/35] [RFR] Migrate inference code to TypeScript --- ...ement.spec.js => InferredElement.spec.tsx} | 4 +-- ...{InferredElement.js => InferredElement.ts} | 11 ++++--- packages/ra-core/src/inference/assertions.js | 31 ----------------- packages/ra-core/src/inference/assertions.ts | 33 +++++++++++++++++++ ...omRecords.js => getElementsFromRecords.ts} | 3 +- ...s.spec.js => getValuesFromRecords.spec.ts} | 0 ...FromRecords.js => getValuesFromRecords.ts} | 2 +- .../src/inference/{index.js => index.ts} | 0 ...pec.js => inferElementFromValues.spec.tsx} | 21 +++++------- ...omValues.js => inferElementFromValues.tsx} | 17 ++++++---- packages/ra-core/src/inference/types.ts | 11 +++++++ 11 files changed, 74 insertions(+), 59 deletions(-) rename packages/ra-core/src/inference/{InferredElement.spec.js => InferredElement.spec.tsx} (90%) rename packages/ra-core/src/inference/{InferredElement.js => InferredElement.ts} (87%) delete mode 100644 packages/ra-core/src/inference/assertions.js create mode 100644 packages/ra-core/src/inference/assertions.ts rename packages/ra-core/src/inference/{getElementsFromRecords.js => getElementsFromRecords.ts} (92%) rename packages/ra-core/src/inference/{getValuesFromRecords.spec.js => getValuesFromRecords.spec.ts} (100%) rename packages/ra-core/src/inference/{getValuesFromRecords.js => getValuesFromRecords.ts} (96%) rename packages/ra-core/src/inference/{index.js => index.ts} (100%) rename packages/ra-core/src/inference/{inferElementFromValues.spec.js => inferElementFromValues.spec.tsx} (95%) rename packages/ra-core/src/inference/{inferElementFromValues.js => inferElementFromValues.tsx} (95%) create mode 100644 packages/ra-core/src/inference/types.ts diff --git a/packages/ra-core/src/inference/InferredElement.spec.js b/packages/ra-core/src/inference/InferredElement.spec.tsx similarity index 90% rename from packages/ra-core/src/inference/InferredElement.spec.js rename to packages/ra-core/src/inference/InferredElement.spec.tsx index 29ba8c59e5f..f2c411ff9ea 100644 --- a/packages/ra-core/src/inference/InferredElement.spec.js +++ b/packages/ra-core/src/inference/InferredElement.spec.tsx @@ -7,7 +7,7 @@ describe('InferredElement', () => { }); describe('getRepresentation', () => { it('should return a default visual representation', () => { - const DummyComponent = () => {}; + const DummyComponent = () => ; const dummyType = { component: DummyComponent }; const ie = new InferredElement(dummyType, { source: 'foo' }); expect(ie.getRepresentation()).toBe( @@ -15,7 +15,7 @@ describe('InferredElement', () => { ); }); it('should return a representation based on the representation type property', () => { - const DummyComponent = () => {}; + const DummyComponent = () => ; const dummyType = { component: DummyComponent, representation: props => `hello, ${props.source}!`, diff --git a/packages/ra-core/src/inference/InferredElement.js b/packages/ra-core/src/inference/InferredElement.ts similarity index 87% rename from packages/ra-core/src/inference/InferredElement.js rename to packages/ra-core/src/inference/InferredElement.ts index c90ff228279..875330da1b2 100644 --- a/packages/ra-core/src/inference/InferredElement.js +++ b/packages/ra-core/src/inference/InferredElement.ts @@ -1,11 +1,12 @@ import { createElement } from 'react'; +import { InferredType } from './types'; class InferredElement { - constructor(type, props, children) { - this.type = type; - this.props = props; - this.children = children; - } + constructor( + private type?: InferredType, + private props?: any, + private children?: any + ) {} getElement(props = {}) { if (!this.isDefined()) { diff --git a/packages/ra-core/src/inference/assertions.js b/packages/ra-core/src/inference/assertions.js deleted file mode 100644 index 0983d7ac921..00000000000 --- a/packages/ra-core/src/inference/assertions.js +++ /dev/null @@ -1,31 +0,0 @@ -import parseDate from 'date-fns/parse'; - -export const isNumeric = value => !isNaN(parseFloat(value)) && isFinite(value); -export const valuesAreNumeric = values => values.every(isNumeric); - -export const isInteger = value => Number.isInteger(value); -export const valuesAreInteger = values => values.every(isInteger); - -export const isBoolean = value => typeof value === 'boolean'; -export const valuesAreBoolean = values => values.every(isBoolean); - -export const isString = value => typeof value === 'string'; -export const valuesAreString = values => values.every(isString); - -const HtmlRegexp = /<([A-Z][A-Z0-9]*)\b[^>]*>(.*?)<\/\1>/i; -export const isHtml = value => HtmlRegexp.test(value); -export const valuesAreHtml = values => values.every(isHtml); - -export const isArray = value => Array.isArray(value); -export const valuesAreArray = values => values.every(isArray); - -export const isDate = value => value instanceof Date; -export const valuesAreDate = values => values.every(isDate); - -export const isDateString = value => - typeof value === 'string' && !isNaN(parseDate(value)); -export const valuesAreDateString = values => values.every(isDateString); - -export const isObject = value => - Object.prototype.toString.call(value) === '[object Object]'; -export const valuesAreObject = values => values.every(isObject); diff --git a/packages/ra-core/src/inference/assertions.ts b/packages/ra-core/src/inference/assertions.ts new file mode 100644 index 00000000000..d5b0468f999 --- /dev/null +++ b/packages/ra-core/src/inference/assertions.ts @@ -0,0 +1,33 @@ +import parseDate from 'date-fns/parse'; + +export const isNumeric = (value: any) => + !isNaN(parseFloat(value)) && isFinite(value); +export const valuesAreNumeric = (values: any[]) => values.every(isNumeric); + +export const isInteger = (value: any) => Number.isInteger(value); +export const valuesAreInteger = (values: any[]) => values.every(isInteger); + +export const isBoolean = (value: any) => typeof value === 'boolean'; +export const valuesAreBoolean = (values: any[]) => values.every(isBoolean); + +export const isString = (value: any) => typeof value === 'string'; +export const valuesAreString = (values: any[]) => values.every(isString); + +const HtmlRegexp = /<([A-Z][A-Z0-9]*)\b[^>]*>(.*?)<\/\1>/i; +export const isHtml = (value: any) => HtmlRegexp.test(value); +export const valuesAreHtml = (values: any[]) => values.every(isHtml); + +export const isArray = (value: any) => Array.isArray(value); +export const valuesAreArray = (values: any[]) => values.every(isArray); + +export const isDate = (value: any) => value instanceof Date; +export const valuesAreDate = (values: any[]) => values.every(isDate); + +export const isDateString = (value: any) => + typeof value === 'string' && !isNaN(parseDate(value).getDate()); +export const valuesAreDateString = (values: any[]) => + values.every(isDateString); + +export const isObject = (value: any) => + Object.prototype.toString.call(value) === '[object Object]'; +export const valuesAreObject = (values: any[]) => values.every(isObject); diff --git a/packages/ra-core/src/inference/getElementsFromRecords.js b/packages/ra-core/src/inference/getElementsFromRecords.ts similarity index 92% rename from packages/ra-core/src/inference/getElementsFromRecords.js rename to packages/ra-core/src/inference/getElementsFromRecords.ts index ed53da7c166..7495cf8a7d7 100644 --- a/packages/ra-core/src/inference/getElementsFromRecords.js +++ b/packages/ra-core/src/inference/getElementsFromRecords.ts @@ -1,5 +1,6 @@ import inferElementFromValues from './inferElementFromValues'; import getValuesFromRecords from './getValuesFromRecords'; +import { InferredTypeMap } from './types'; /** * Get a list of React-admin field components from a list of records @@ -32,7 +33,7 @@ import getValuesFromRecords from './getValuesFromRecords'; * // , * // ]; */ -export default (records, types) => { +export default (records: any[], types: InferredTypeMap) => { const fieldValues = getValuesFromRecords(records); return Object.keys(fieldValues) .reduce( diff --git a/packages/ra-core/src/inference/getValuesFromRecords.spec.js b/packages/ra-core/src/inference/getValuesFromRecords.spec.ts similarity index 100% rename from packages/ra-core/src/inference/getValuesFromRecords.spec.js rename to packages/ra-core/src/inference/getValuesFromRecords.spec.ts diff --git a/packages/ra-core/src/inference/getValuesFromRecords.js b/packages/ra-core/src/inference/getValuesFromRecords.ts similarity index 96% rename from packages/ra-core/src/inference/getValuesFromRecords.js rename to packages/ra-core/src/inference/getValuesFromRecords.ts index a1192404d38..04de5bbb7db 100644 --- a/packages/ra-core/src/inference/getValuesFromRecords.js +++ b/packages/ra-core/src/inference/getValuesFromRecords.ts @@ -24,7 +24,7 @@ * // user_id: [123, 456], * // } */ -export default records => +export default (records: any[]) => records.reduce((values, record) => { Object.keys(record).forEach(fieldName => { if (!values[fieldName]) { diff --git a/packages/ra-core/src/inference/index.js b/packages/ra-core/src/inference/index.ts similarity index 100% rename from packages/ra-core/src/inference/index.js rename to packages/ra-core/src/inference/index.ts diff --git a/packages/ra-core/src/inference/inferElementFromValues.spec.js b/packages/ra-core/src/inference/inferElementFromValues.spec.tsx similarity index 95% rename from packages/ra-core/src/inference/inferElementFromValues.spec.js rename to packages/ra-core/src/inference/inferElementFromValues.spec.tsx index 02cac5b057d..8bc76112a28 100644 --- a/packages/ra-core/src/inference/inferElementFromValues.spec.js +++ b/packages/ra-core/src/inference/inferElementFromValues.spec.tsx @@ -1,11 +1,15 @@ -import React from 'react'; +import React, { SFC } from 'react'; import inferElementFromValues from './inferElementFromValues'; import InferredElement from './InferredElement'; +interface Props { + source: string; + reference?: string; +} describe('inferElementFromValues', () => { - function Good() {} - function Bad() {} - function Dummy() {} + const Good: SFC = () => ; + const Bad: SFC = () => ; + const Dummy: SFC<{ [key: string]: any }> = () => ; it('should return an InferredElement', () => { const types = { @@ -23,15 +27,6 @@ describe('inferElementFromValues', () => { inferElementFromValues('id', ['foo'], types).getElement() ).toEqual(); }); - it('should return null if type is falsy', () => { - const types = { - id: false, - string: { component: Bad }, - }; - expect( - inferElementFromValues('id', ['foo'], types).getElement() - ).toBeUndefined(); - }); it('should return an id field for field named id', () => { const types = { id: { component: Good }, diff --git a/packages/ra-core/src/inference/inferElementFromValues.js b/packages/ra-core/src/inference/inferElementFromValues.tsx similarity index 95% rename from packages/ra-core/src/inference/inferElementFromValues.js rename to packages/ra-core/src/inference/inferElementFromValues.tsx index 2ff576c72f9..bb0c32075be 100644 --- a/packages/ra-core/src/inference/inferElementFromValues.js +++ b/packages/ra-core/src/inference/inferElementFromValues.tsx @@ -16,6 +16,7 @@ import { valuesAreObject, valuesAreString, } from './assertions'; +import { InferredTypeMap } from './types'; const DefaultComponent = () => ;; const defaultType = { @@ -76,7 +77,11 @@ const hasType = (type, types) => typeof types[type] !== 'undefined'; * * @return InferredElement */ -const inferElementFromValues = (name, values = [], types = defaultTypes) => { +const inferElementFromValues = ( + name, + values = [], + types: InferredTypeMap = defaultTypes +) => { if (name === 'id' && hasType('id', types)) { return new InferredElement(types.id, { source: name }); } @@ -88,7 +93,7 @@ const inferElementFromValues = (name, values = [], types = defaultTypes) => { types.reference, { source: name, - reference: reference, + reference, }, new InferredElement(types.referenceChild) ) @@ -102,7 +107,7 @@ const inferElementFromValues = (name, values = [], types = defaultTypes) => { types.reference, { source: name, - reference: reference, + reference, }, new InferredElement(types.referenceChild) ) @@ -119,7 +124,7 @@ const inferElementFromValues = (name, values = [], types = defaultTypes) => { types.referenceArray, { source: name, - reference: reference, + reference, }, new InferredElement(types.referenceArrayChild) ) @@ -136,13 +141,13 @@ const inferElementFromValues = (name, values = [], types = defaultTypes) => { types.referenceArray, { source: name, - reference: reference, + reference, }, new InferredElement(types.referenceArrayChild) ) ); } - if (values.length == 0) { + if (values.length === 0) { // FIXME introspect further using name return new InferredElement(types.string, { source: name }); } diff --git a/packages/ra-core/src/inference/types.ts b/packages/ra-core/src/inference/types.ts new file mode 100644 index 00000000000..185c2c80ba5 --- /dev/null +++ b/packages/ra-core/src/inference/types.ts @@ -0,0 +1,11 @@ +import { ComponentType } from 'react'; + +export interface InferredType { + type?: ComponentType; + component?: ComponentType; + representation?: (props: any, children: any) => string; +} + +export interface InferredTypeMap { + [key: string]: InferredType; +} From 59e179c62fd4ac1a12f7327fa4a7dea06e0f4df6 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 14:29:46 +0100 Subject: [PATCH 10/35] Review --- packages/ra-core/src/form/FormDataConsumer.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/ra-core/src/form/FormDataConsumer.tsx b/packages/ra-core/src/form/FormDataConsumer.tsx index 3073820f562..570b46efc3a 100644 --- a/packages/ra-core/src/form/FormDataConsumer.tsx +++ b/packages/ra-core/src/form/FormDataConsumer.tsx @@ -13,6 +13,14 @@ interface ChildrenFunctionParams { getSource?: (source: string) => string; } +interface ConnectedProps { + children: (params: ChildrenFunctionParams) => ReactNode; + form: string; + record?: any; + source: string; + [key: string]: any; +} + interface Props extends ConnectedProps { formData: any; index?: number; @@ -112,13 +120,6 @@ export const FormDataConsumerView: SFC = ({ return ret === undefined ? null : ret; }; -interface ConnectedProps { - children: (params: ChildrenFunctionParams) => ReactNode; - form: string; - record?: any; - source: string; -} - const mapStateToProps = ( state: ReduxState, { form, record }: ConnectedProps From b2ea565fc83b94fb702b4c1e185c3e2451169d7d Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 14:39:01 +0100 Subject: [PATCH 11/35] Better memoize function typings --- packages/ra-core/src/form/validate.ts | 66 ++++++++++----------------- 1 file changed, 23 insertions(+), 43 deletions(-) diff --git a/packages/ra-core/src/form/validate.ts b/packages/ra-core/src/form/validate.ts index e7d4c854edb..d5bc37e22be 100644 --- a/packages/ra-core/src/form/validate.ts +++ b/packages/ra-core/src/form/validate.ts @@ -45,14 +45,17 @@ const getMessage = ( ...messageArgs, }); +type Memoize = any>( + func: T, + resolver?: (...args: any[]) => any +) => T; + // If we define validation functions directly in JSX, it will // result in a new function at every render, and then trigger infinite re-render. // Hence, we memoize every built-in validator to prevent a "Maximum call stack" error. -const memoize = (fn: any) => +const memoize: Memoize = (fn: any) => lodashMemoize(fn, (...args) => JSON.stringify(args)); -type Required = (message?: string | MessageFunc) => Validator; - /** * Required validator * @@ -65,19 +68,16 @@ type Required = (message?: string | MessageFunc) => Validator; * const titleValidators = [required('The title is required')]; * */ -export const required: Required = memoize( - (message = 'ra.validation.required') => - Object.assign( - (value, values, props) => - isEmpty(value) - ? getMessage(message, undefined, value, values, props) - : undefined, - { isRequired: true } - ) +export const required = memoize((message = 'ra.validation.required') => + Object.assign( + (value, values, props) => + isEmpty(value) + ? getMessage(message, undefined, value, values, props) + : undefined, + { isRequired: true } + ) ); -type MinLength = (min: number, message?: string | MessageFunc) => Validator; - /** * Minimum length validator * @@ -91,15 +91,13 @@ type MinLength = (min: number, message?: string | MessageFunc) => Validator; * const passwordValidators = [minLength(10, 'Should be at least 10 characters')]; * */ -export const minLength: MinLength = memoize( +export const minLength = memoize( (min, message = 'ra.validation.minLength') => (value, values, props) => !isEmpty(value) && value.length < min ? getMessage(message, { min }, value, values, props) : undefined ); -type MaxLength = (max: number, message?: string | MessageFunc) => Validator; - /** * Maximum length validator * @@ -113,15 +111,13 @@ type MaxLength = (max: number, message?: string | MessageFunc) => Validator; * const nameValidators = [maxLength(10, 'Should be at most 10 characters')]; * */ -export const maxLength: MaxLength = memoize( +export const maxLength = memoize( (max, message = 'ra.validation.maxLength') => (value, values, props) => !isEmpty(value) && value.length > max ? getMessage(message, { max }, value, values, props) : undefined ); -type MinValue = (min: number, message?: string | MessageFunc) => Validator; - /** * Minimum validator * @@ -135,15 +131,13 @@ type MinValue = (min: number, message?: string | MessageFunc) => Validator; * const fooValidators = [minValue(5, 'Should be more than 5')]; * */ -export const minValue: MinValue = memoize( +export const minValue = memoize( (min, message = 'ra.validation.minValue') => (value, values, props) => !isEmpty(value) && value < min ? getMessage(message, { min }, value, values, props) : undefined ); -type MaxValue = (max: number, message?: string | MessageFunc) => Validator; - /** * Maximum validator * @@ -157,15 +151,13 @@ type MaxValue = (max: number, message?: string | MessageFunc) => Validator; * const fooValidators = [maxValue(10, 'Should be less than 10')]; * */ -export const maxValue: MaxValue = memoize( +export const maxValue = memoize( (max, message = 'ra.validation.maxValue') => (value, values, props) => !isEmpty(value) && value > max ? getMessage(message, { max }, value, values, props) : undefined ); -type NumberValidator = (message?: string | MessageFunc) => Validator; - /** * Number validator * @@ -179,18 +171,13 @@ type NumberValidator = (message?: string | MessageFunc) => Validator; * */ // tslint:disable-next-line:variable-name -export const number: NumberValidator = memoize( +export const number = memoize( (message = 'ra.validation.number') => (value, values, props) => !isEmpty(value) && isNaN(Number(value)) ? getMessage(message, undefined, value, values, props) : undefined ); -type RegedValidator = ( - pattern: RegExp, - message?: string | MessageFunc -) => Validator; - /** * Regular expression validator * @@ -204,7 +191,7 @@ type RegedValidator = ( * const zipValidators = [regex(/^\d{5}(?:[-\s]\d{4})?$/, 'Must be a zip code')]; * */ -export const regex: RegedValidator = lodashMemoize( +export const regex = lodashMemoize( (pattern, message = 'ra.validation.regex') => (value, values, props) => !isEmpty(value) && typeof value === 'string' && !pattern.test(value) ? getMessage(message, { pattern }, value, values, props) @@ -214,8 +201,6 @@ export const regex: RegedValidator = lodashMemoize( } ); -type EmailValidator = (message?: string | MessageFunc) => Validator; - /** * Email validator * @@ -228,8 +213,8 @@ type EmailValidator = (message?: string | MessageFunc) => Validator; * const emailValidators = [email('Must be an email')]; * */ -export const email: EmailValidator = memoize( - (message = 'ra.validation.email') => regex(EMAIL_REGEX, message) +export const email = memoize((message = 'ra.validation.email') => + regex(EMAIL_REGEX, message) ); const oneOfTypeMessage: MessageFunc = ({ list, value, values, translate }) => @@ -237,11 +222,6 @@ const oneOfTypeMessage: MessageFunc = ({ list, value, values, translate }) => options: list.join(', '), }); -type ChoicesValidator = ( - list: any[], - message?: string | MessageFunc -) => Validator; - /** * Choices validator * @@ -255,7 +235,7 @@ type ChoicesValidator = ( * const genderValidators = [choices(['male', 'female'], 'Must be either Male or Female')]; * */ -export const choices: ChoicesValidator = memoize( +export const choices = memoize( (list, message = oneOfTypeMessage) => (value, values, props) => !isEmpty(value) && list.indexOf(value) === -1 ? getMessage(message, { list }, value, values, props) From 5fda66c2a7d2521f8dd9833b2cafb981be923840 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 15:30:32 +0100 Subject: [PATCH 12/35] [WIP] Migrate ra-core controllers to TypeScript - [x] Fields - [ ] Inputs - [ ] Views --- .../ra-core/src/actions/accumulateActions.ts | 7 +- .../dataActions/crudGetManyReference.ts | 11 +++ ...=> ReferenceArrayFieldController.spec.tsx} | 24 ++--- ...r.js => ReferenceArrayFieldController.tsx} | 62 +++++++----- ...c.js => ReferenceFieldController.spec.tsx} | 44 +++++---- ...roller.js => ReferenceFieldController.tsx} | 76 +++++++------- ... => ReferenceManyFieldController.spec.tsx} | 15 ++- ...er.js => ReferenceManyFieldController.tsx} | 99 +++++++++++-------- .../src/reducer/admin/references/oneToMany.ts | 6 +- packages/ra-core/src/types.ts | 4 + 10 files changed, 208 insertions(+), 140 deletions(-) rename packages/ra-core/src/controller/field/{ReferenceArrayFieldController.spec.js => ReferenceArrayFieldController.spec.tsx} (82%) rename packages/ra-core/src/controller/field/{ReferenceArrayFieldController.js => ReferenceArrayFieldController.tsx} (67%) rename packages/ra-core/src/controller/field/{ReferenceFieldController.spec.js => ReferenceFieldController.spec.tsx} (81%) rename packages/ra-core/src/controller/field/{ReferenceFieldController.js => ReferenceFieldController.tsx} (67%) rename packages/ra-core/src/controller/field/{ReferenceManyFieldController.spec.js => ReferenceManyFieldController.spec.tsx} (89%) rename packages/ra-core/src/controller/field/{ReferenceManyFieldController.js => ReferenceManyFieldController.tsx} (74%) diff --git a/packages/ra-core/src/actions/accumulateActions.ts b/packages/ra-core/src/actions/accumulateActions.ts index c1342001ac1..1ddb402bac1 100644 --- a/packages/ra-core/src/actions/accumulateActions.ts +++ b/packages/ra-core/src/actions/accumulateActions.ts @@ -6,16 +6,19 @@ export interface CrudGetManyAccumulateAction { readonly type: typeof CRUD_GET_MANY_ACCUMULATE; readonly payload: { resource: string; - ids: []; + ids: any[]; }; readonly meta: { accumulate: any; }; } +// Used to type the dispatcher function (the one injected by mapDispatchToProps) +export type CrudGetManyAccumulate = (resource: string, ids: any[]) => void; + export const crudGetManyAccumulate = ( resource: string, - ids: [] + ids: any[] ): CrudGetManyAccumulateAction => ({ type: CRUD_GET_MANY_ACCUMULATE, payload: { resource, ids }, diff --git a/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts b/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts index a0dc625af53..53b8dd62bdf 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts @@ -3,6 +3,17 @@ import { GET_MANY_REFERENCE } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; +export type CrudGetManyReference = ( + reference: string, + target: string, + id: Identifier, + relatedTo: string, + pagination: Pagination, + sort: Sort, + filter: object, + source: string +) => void; + export const crudGetManyReference = ( reference: string, target: string, diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.js b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx similarity index 82% rename from packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.js rename to packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx index 854cae6c778..4dbbaf8e7f6 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.js +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx @@ -1,22 +1,24 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; -import { ReferenceArrayFieldController } from './ReferenceArrayFieldController'; +import { ReferenceArrayFieldControllerView as ReferenceArrayFieldController } from './ReferenceArrayFieldController'; describe('', () => { + const crudGetManyAccumulate = jest.fn(); + it('should set the loadedOnce prop to false when related records are not yet fetched', () => { const children = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -29,14 +31,14 @@ describe('', () => { shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -53,14 +55,14 @@ describe('', () => { }; shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -78,14 +80,14 @@ describe('', () => { }; shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -103,14 +105,14 @@ describe('', () => { }; shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.js b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx similarity index 67% rename from packages/ra-core/src/controller/field/ReferenceArrayFieldController.js rename to packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx index 6e6b804a91b..811b0560b3b 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.js +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx @@ -1,10 +1,33 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import get from 'lodash/get'; -import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions'; +import { + crudGetManyAccumulate as crudGetManyAccumulateAction, + CrudGetManyAccumulate, +} from '../../actions'; import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; +import { ReduxState, Record, RecordMap } from '../../types'; + +interface ChildrenFuncParams { + loadedOnce: boolean; + ids: any[]; + data: RecordMap; + referenceBasePath: string; + currentSort: any; +} + +interface Props { + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetManyAccumulate: CrudGetManyAccumulate; + data?: RecordMap; + ids: any[]; + record?: Record; + reference: string; + resource: string; + source: string; +} /** * A container component that fetches records from another resource specified @@ -38,13 +61,16 @@ import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; * * */ -export class ReferenceArrayFieldController extends Component { +export class ReferenceArrayFieldControllerView extends Component { componentDidMount() { this.fetchReferences(); } componentWillReceiveProps(nextProps) { - if ((this.props.record || {}).id !== (nextProps.record || {}).id) { + if ( + (this.props.record || { id: undefined }).id !== + (nextProps.record || {}).id + ) { this.fetchReferences(nextProps); } } @@ -66,6 +92,7 @@ export class ReferenceArrayFieldController extends Component { const referenceBasePath = basePath.replace(resource, reference); // FIXME obviously very weak return children({ + // tslint:disable-next-line:triple-equals loadedOnce: data != undefined, ids, data, @@ -75,24 +102,7 @@ export class ReferenceArrayFieldController extends Component { } } -ReferenceArrayFieldController.propTypes = { - addLabel: PropTypes.bool, - basePath: PropTypes.string.isRequired, - classes: PropTypes.object, - className: PropTypes.string, - children: PropTypes.func.isRequired, - crudGetManyAccumulate: PropTypes.func.isRequired, - data: PropTypes.object, - ids: PropTypes.array.isRequired, - label: PropTypes.string, - record: PropTypes.object.isRequired, - reference: PropTypes.string.isRequired, - resource: PropTypes.string.isRequired, - sortBy: PropTypes.string, - source: PropTypes.string.isRequired, -}; - -const mapStateToProps = (state, props) => { +const mapStateToProps = (state: ReduxState, props: Props) => { const { record, source, reference } = props; const ids = get(record, source) || []; return { @@ -101,9 +111,11 @@ const mapStateToProps = (state, props) => { }; }; -export default connect( +const ReferenceArrayFieldController = connect( mapStateToProps, { crudGetManyAccumulate: crudGetManyAccumulateAction, } -)(ReferenceArrayFieldController); +)(ReferenceArrayFieldControllerView); + +export default ReferenceArrayFieldController; diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.js b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx similarity index 81% rename from packages/ra-core/src/controller/field/ReferenceFieldController.spec.js rename to packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx index 1b6974bdaed..13fd50b1efd 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.js +++ b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; -import { ReferenceFieldController } from './ReferenceFieldController'; +import { ReferenceFieldControllerView as ReferenceFieldController } from './ReferenceFieldController'; describe('', () => { it('should call crudGetManyAccumulate on componentDidMount if reference source is defined', () => { @@ -10,7 +10,7 @@ describe('', () => { shallow( ', () => { shallow( ', () => { }); it('should render a link to the Edit page of the related record by default', () => { const children = jest.fn(); + const crudGetManyAccumulate = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -54,15 +55,16 @@ describe('', () => { }); it('should render a link to the Edit page of the related record when the resource contains slashes', () => { const children = jest.fn(); + const crudGetManyAccumulate = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -74,15 +76,16 @@ describe('', () => { }); it('should render a link to the Edit page of the related record when the resource is named edit or show', () => { const children = jest.fn(); + const crudGetManyAccumulate = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -91,13 +94,13 @@ describe('', () => { shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -106,16 +109,17 @@ describe('', () => { }); it('should render a link to the Show page of the related record when the linkType is show', () => { const children = jest.fn(); + const crudGetManyAccumulate = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -127,16 +131,17 @@ describe('', () => { }); it('should render a link to the Show page of the related record when the resource is named edit or show and linkType is show', () => { const children = jest.fn(); + const crudGetManyAccumulate = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -148,14 +153,14 @@ describe('', () => { shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} @@ -168,15 +173,16 @@ describe('', () => { }); it('should render no link when the linkType is false', () => { const children = jest.fn(); + const crudGetManyAccumulate = jest.fn(); shallow( {}} + crudGetManyAccumulate={crudGetManyAccumulate} > {children} diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.js b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx similarity index 67% rename from packages/ra-core/src/controller/field/ReferenceFieldController.js rename to packages/ra-core/src/controller/field/ReferenceFieldController.tsx index 5e69a21cc1a..03dbfccab78 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldController.js +++ b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx @@ -1,10 +1,32 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import get from 'lodash/get'; -import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions'; +import { + crudGetManyAccumulate as crudGetManyAccumulateAction, + CrudGetManyAccumulate, +} from '../../actions'; import { linkToRecord } from '../../util'; +import { Record } from '../../types'; + +interface ChildrenFuncParams { + isLoading: boolean; + referenceRecord: Record; + resourceLinkPath: string | boolean; +} + +interface Props { + allowEmpty?: boolean; + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetManyAccumulate: CrudGetManyAccumulate; + record?: Record; + reference: string; + referenceRecord?: Record; + resource: string; + source: string; + linkType: string | boolean; +} /** * Fetch reference record, and delegate rendering to child component. @@ -35,7 +57,14 @@ import { linkToRecord } from '../../util'; * * */ -export class ReferenceFieldController extends Component { +export class ReferenceFieldControllerView extends Component { + public static defaultProps: Partial = { + allowEmpty: false, + linkType: 'edit', + referenceRecord: null, + record: { id: '' }, + }; + componentDidMount() { this.fetchReference(this.props); } @@ -68,7 +97,8 @@ export class ReferenceFieldController extends Component { const rootPath = basePath.replace(resource, reference); const resourceLinkPath = !linkType ? false - : linkToRecord(rootPath, get(record, source), linkType); + : linkToRecord(rootPath, get(record, source), linkType as string); + return children({ isLoading: !referenceRecord && !allowEmpty, referenceRecord, @@ -77,36 +107,6 @@ export class ReferenceFieldController extends Component { } } -ReferenceFieldController.propTypes = { - addLabel: PropTypes.bool, - allowEmpty: PropTypes.bool.isRequired, - basePath: PropTypes.string.isRequired, - children: PropTypes.func.isRequired, - classes: PropTypes.object, - className: PropTypes.string, - cellClassName: PropTypes.string, - headerClassName: PropTypes.string, - crudGetManyAccumulate: PropTypes.func.isRequired, - label: PropTypes.string, - record: PropTypes.object, - reference: PropTypes.string.isRequired, - referenceRecord: PropTypes.object, - resource: PropTypes.string, - sortBy: PropTypes.string, - source: PropTypes.string.isRequired, - translateChoice: PropTypes.func, - linkType: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]) - .isRequired, -}; - -ReferenceFieldController.defaultProps = { - allowEmpty: false, - classes: {}, - linkType: 'edit', - referenceRecord: null, - record: {}, -}; - const mapStateToProps = (state, props) => ({ referenceRecord: state.admin.resources[props.reference] && @@ -115,9 +115,11 @@ const mapStateToProps = (state, props) => ({ ], }); -export default connect( +const ReferenceFieldController = connect( mapStateToProps, { crudGetManyAccumulate: crudGetManyAccumulateAction, } -)(ReferenceFieldController); +)(ReferenceFieldControllerView); + +export default ReferenceFieldController; diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.js b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx similarity index 89% rename from packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.js rename to packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 305c0a1a681..4c8e2141410 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.js +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -2,18 +2,19 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; import { render } from 'react-testing-library'; -import { ReferenceManyFieldController } from './ReferenceManyFieldController'; +import { ReferenceManyFieldControllerView as ReferenceManyFieldController } from './ReferenceManyFieldController'; describe('', () => { it('should set loadedOnce to false when related records are not yet fetched', () => { const children = jest.fn(); + const crudGetManyReference = jest.fn(); shallow( {}} + crudGetManyReference={crudGetManyReference} > {children} , @@ -24,6 +25,7 @@ describe('', () => { it('should pass data and ids to children function', () => { const children = jest.fn(); + const crudGetManyReference = jest.fn(); const data = { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, @@ -36,7 +38,7 @@ describe('', () => { basePath="" data={data} ids={[1, 2]} - crudGetManyReference={() => {}} + crudGetManyReference={crudGetManyReference} > {children} , @@ -48,6 +50,7 @@ describe('', () => { it('should support record with string identifier', () => { const children = jest.fn(); + const crudGetManyReference = jest.fn(); const data = { 'abc-1': { id: 'abc-1', title: 'hello' }, 'abc-2': { id: 'abc-2', title: 'world' }, @@ -60,7 +63,7 @@ describe('', () => { basePath="" data={data} ids={['abc-1', 'abc-2']} - crudGetManyReference={() => {}} + crudGetManyReference={crudGetManyReference} > {children} , @@ -72,6 +75,7 @@ describe('', () => { it('should support record with number identifier', () => { const children = jest.fn(); + const crudGetManyReference = jest.fn(); const data = { 1: { id: 1, title: 'hello' }, 2: { id: 2, title: 'world' }, @@ -84,7 +88,7 @@ describe('', () => { basePath="" data={data} ids={[1, 2]} - crudGetManyReference={() => {}} + crudGetManyReference={crudGetManyReference} > {children} , @@ -147,6 +151,7 @@ describe('', () => { { page: 1, perPage: 25 }, { field: 'id', order: 'ASC' }, {}, + 'id', ]); }); }); diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.js b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx similarity index 74% rename from packages/ra-core/src/controller/field/ReferenceManyFieldController.js rename to packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index 321641f4342..6193c638ccb 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.js +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -1,9 +1,12 @@ -import { Component } from 'react'; +import { Component, ReactNode } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import isEqual from 'lodash/isEqual'; -import { crudGetManyReference as crudGetManyReferenceAction } from '../../actions'; +import { + crudGetManyReference as crudGetManyReferenceAction, + CrudGetManyReference, +} from '../../actions'; import { SORT_ASC, SORT_DESC, @@ -14,6 +17,39 @@ import { getTotal, nameRelatedTo, } from '../../reducer/admin/references/oneToMany'; +import { Record, Sort, RecordMap, Identifier } from '../../types'; + +interface ChildrenFuncParams { + currentSort: Sort; + data: RecordMap; + ids: Identifier[]; + loadedOnce: boolean; + page: number; + perPage: number; + referenceBasePath: string; + setPage: (page: number) => void; + setPerPage: (perPage: number) => void; + setSort: (field: string) => void; + total: number; +} + +interface Props { + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetManyReference: CrudGetManyReference; + data?: RecordMap; + filter?: any; + ids?: any[]; + loadedOnce?: boolean; + perPage?: number; + record?: Record; + reference: string; + resource: string; + sort?: Sort; + source: string; + target: string; + total?: number; +} /** * Render related records to the current one. @@ -61,11 +97,19 @@ import { * ... * */ -export class ReferenceManyFieldController extends Component { - constructor(props) { - super(props); - this.state = { sort: props.sort, page: 1, perPage: props.perPage }; - } +export class ReferenceManyFieldControllerView extends Component { + public static defaultProps: Partial = { + filter: {}, + perPage: 25, + sort: { field: 'id', order: 'DESC' }, + source: 'id', + }; + + public state = { + sort: this.props.sort, + page: 1, + perPage: this.props.perPage, + }; componentDidMount() { this.fetchReferences(); @@ -84,7 +128,7 @@ export class ReferenceManyFieldController extends Component { } } - setSort = field => { + setSort = (field: string) => { const order = this.state.sort.field === field && this.state.sort.order === SORT_ASC @@ -109,6 +153,7 @@ export class ReferenceManyFieldController extends Component { target, filter ); + crudGetManyReference( reference, target, @@ -116,7 +161,8 @@ export class ReferenceManyFieldController extends Component { relatedTo, { page, perPage }, sort, - filter + filter, + source ); } @@ -150,35 +196,6 @@ export class ReferenceManyFieldController extends Component { } } -ReferenceManyFieldController.propTypes = { - basePath: PropTypes.string.isRequired, - children: PropTypes.func.isRequired, - crudGetManyReference: PropTypes.func.isRequired, - filter: PropTypes.object, - ids: PropTypes.array, - perPage: PropTypes.number, - record: PropTypes.object, - reference: PropTypes.string.isRequired, - data: PropTypes.object, - loadedOnce: PropTypes.bool, - resource: PropTypes.string.isRequired, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(['ASC', 'DESC']), - }), - sortBy: PropTypes.string, - source: PropTypes.string.isRequired, - target: PropTypes.string.isRequired, - total: PropTypes.number, -}; - -ReferenceManyFieldController.defaultProps = { - filter: {}, - perPage: 25, - sort: { field: 'id', order: 'DESC' }, - source: 'id', -}; - function mapStateToProps(state, props) { const relatedTo = nameRelatedTo( props.reference, @@ -194,9 +211,11 @@ function mapStateToProps(state, props) { }; } -export default connect( +const ReferenceManyFieldController = connect( mapStateToProps, { crudGetManyReference: crudGetManyReferenceAction, } -)(ReferenceManyFieldController); +)(ReferenceManyFieldControllerView); + +export default ReferenceManyFieldController; diff --git a/packages/ra-core/src/reducer/admin/references/oneToMany.ts b/packages/ra-core/src/reducer/admin/references/oneToMany.ts index 07c475f8f82..964ff2dc06b 100644 --- a/packages/ra-core/src/reducer/admin/references/oneToMany.ts +++ b/packages/ra-core/src/reducer/admin/references/oneToMany.ts @@ -75,7 +75,11 @@ export const getReferences = (state: ReduxState, reference, relatedTo) => { }, {}); }; -export const getReferencesByIds = (state: ReduxState, reference, ids) => { +export const getReferencesByIds = ( + state: ReduxState, + reference: string, + ids: any[] +) => { if (ids.length === 0) { return {}; } diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index c86ce6a541e..f129ca69401 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -4,6 +4,10 @@ export interface Record { [key: string]: any; } +export interface RecordMap { + [id: string]: Record; +} + export interface Sort { field: string; order: string; From 7dc064bc614c808bce472760cd433d69aeaf4680 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 15:37:18 +0100 Subject: [PATCH 13/35] Missing index file --- packages/ra-core/src/controller/field/{index.js => index.ts} | 1 + 1 file changed, 1 insertion(+) rename packages/ra-core/src/controller/field/{index.js => index.ts} (99%) diff --git a/packages/ra-core/src/controller/field/index.js b/packages/ra-core/src/controller/field/index.ts similarity index 99% rename from packages/ra-core/src/controller/field/index.js rename to packages/ra-core/src/controller/field/index.ts index f1eb2aaf8f0..fc57420b5d3 100644 --- a/packages/ra-core/src/controller/field/index.js +++ b/packages/ra-core/src/controller/field/index.ts @@ -1,6 +1,7 @@ import ReferenceArrayFieldController from './ReferenceArrayFieldController'; import ReferenceFieldController from './ReferenceFieldController'; import ReferenceManyFieldController from './ReferenceManyFieldController'; + export { ReferenceArrayFieldController, ReferenceFieldController, From 7678936c7ade424dc5fe5532152cd92418c48804 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 16:22:48 +0100 Subject: [PATCH 14/35] Inputs --- .../ra-core/src/actions/accumulateActions.ts | 13 +- .../src/actions/dataActions/crudGetMany.ts | 2 + .../actions/dataActions/crudGetMatching.ts | 8 ++ .../field/ReferenceManyFieldController.tsx | 1 - ...=> ReferenceArrayInputController.spec.tsx} | 9 +- ...r.js => ReferenceArrayInputController.tsx} | 131 +++++++++-------- ...c.js => ReferenceInputController.spec.tsx} | 14 +- ...roller.js => ReferenceInputController.tsx} | 132 +++++++++++------- .../controller/input/{index.js => index.ts} | 0 ...us.spec.js => referenceDataStatus.spec.ts} | 32 ++--- ...ceDataStatus.js => referenceDataStatus.ts} | 67 ++++++--- .../ra-core/src/controller/input/types.ts | 3 + 12 files changed, 250 insertions(+), 162 deletions(-) rename packages/ra-core/src/controller/input/{ReferenceArrayInputController.spec.js => ReferenceArrayInputController.spec.tsx} (98%) rename packages/ra-core/src/controller/input/{ReferenceArrayInputController.js => ReferenceArrayInputController.tsx} (77%) rename packages/ra-core/src/controller/input/{ReferenceInputController.spec.js => ReferenceInputController.spec.tsx} (98%) rename packages/ra-core/src/controller/input/{ReferenceInputController.js => ReferenceInputController.tsx} (75%) rename packages/ra-core/src/controller/input/{index.js => index.ts} (100%) rename packages/ra-core/src/controller/input/{referenceDataStatus.spec.js => referenceDataStatus.spec.ts} (94%) rename packages/ra-core/src/controller/input/{referenceDataStatus.js => referenceDataStatus.ts} (68%) create mode 100644 packages/ra-core/src/controller/input/types.ts diff --git a/packages/ra-core/src/actions/accumulateActions.ts b/packages/ra-core/src/actions/accumulateActions.ts index 1ddb402bac1..8181e3d9c44 100644 --- a/packages/ra-core/src/actions/accumulateActions.ts +++ b/packages/ra-core/src/actions/accumulateActions.ts @@ -1,4 +1,5 @@ import { crudGetMany, crudGetMatching } from './dataActions'; +import { Pagination, Sort } from '../types'; export const CRUD_GET_MANY_ACCUMULATE = 'RA/CRUD_GET_MANY_ACCUMULATE'; @@ -36,11 +37,19 @@ export interface CrudGetMatchingAccumulateAction { }; } +export type CrudGetMatchingAccumulate = ( + reference: string, + relatedTo: string, + pagination: Pagination, + sort: Sort, + filter: object +) => void; + export const crudGetMatchingAccumulate = ( reference: string, relatedTo: string, - pagination: { page: number; perPage: number }, - sort: { field: string; order: string }, + pagination: Pagination, + sort: Sort, filter: object ): CrudGetMatchingAccumulateAction => { const action = crudGetMatching( diff --git a/packages/ra-core/src/actions/dataActions/crudGetMany.ts b/packages/ra-core/src/actions/dataActions/crudGetMany.ts index edf3fe78f35..ee4edfe5ce3 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetMany.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetMany.ts @@ -3,6 +3,8 @@ import { GET_MANY } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; +export type CrudGetMany = (resource: string, ids: Identifier[]) => void; + export const crudGetMany = ( resource: string, ids: Identifier[] diff --git a/packages/ra-core/src/actions/dataActions/crudGetMatching.ts b/packages/ra-core/src/actions/dataActions/crudGetMatching.ts index ad3bfce9f78..02eb7812f93 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetMatching.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetMatching.ts @@ -3,6 +3,14 @@ import { GET_LIST } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; +export type CrudGetMatching = ( + reference: string, + relatedTo: string, + pagination: Pagination, + sort: Sort, + filter: object +) => void; + export const crudGetMatching = ( reference: string, relatedTo: string, diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index 6193c638ccb..1ff8344547f 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -1,5 +1,4 @@ import { Component, ReactNode } from 'react'; -import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import isEqual from 'lodash/isEqual'; diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.js b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx similarity index 98% rename from packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.js rename to packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx index 35711c9ac6b..ba2cab9731e 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.js +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx @@ -1,17 +1,18 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; -import { ReferenceArrayInputController } from './ReferenceArrayInputController'; +import { ReferenceArrayInputControllerView as ReferenceArrayInputController } from './ReferenceArrayInputController'; describe('', () => { const defaultProps = { children: jest.fn(), crudGetMatching: () => true, crudGetMany: () => true, - input: {}, + input: { value: undefined }, matchingReferences: [], meta: {}, - record: {}, + record: undefined, + basePath: '/tags', reference: 'tags', resource: 'posts', source: 'tag_ids', @@ -25,7 +26,6 @@ describe('', () => { {...{ ...defaultProps, matchingReferences: null, - input: {}, }} > {children} @@ -77,7 +77,6 @@ describe('', () => { {...{ ...defaultProps, matchingReferences: { error: 'fetch error' }, - input: {}, referenceRecords: [], }} > diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.js b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx similarity index 77% rename from packages/ra-core/src/controller/input/ReferenceArrayInputController.js rename to packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx index 23174161797..69680f9328a 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.js +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx @@ -1,5 +1,4 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import debounce from 'lodash/debounce'; import compose from 'recompose/compose'; @@ -9,6 +8,8 @@ import isEqual from 'lodash/isEqual'; import { crudGetMany as crudGetManyAction, crudGetMatching as crudGetMatchingAction, + CrudGetMatching, + CrudGetMany, } from '../../actions/dataActions'; import { getPossibleReferences, @@ -16,9 +17,48 @@ import { getReferenceResource, } from '../../reducer'; import { getStatusForArrayInput as getDataStatus } from './referenceDataStatus'; -import translate from '../../i18n/translate'; +import withTranslate from '../../i18n/translate'; +import { Record, Sort, Translate, Pagination } from '../../types'; +import { MatchingReferencesError } from './types'; -const referenceSource = (resource, source) => `${resource}@${source}`; +const defaultReferenceSource = (resource: string, source: string) => + `${resource}@${source}`; + +interface ChildrenFuncParams { + choices: Record[]; + error?: string; + isLoading: boolean; + onChange: (value: any) => void; + setFilter: (filter: any) => void; + setPagination: (pagination: Pagination) => void; + setSort: (sort: Sort) => void; + warning?: string; +} + +interface Props { + allowEmpty: boolean; + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetMatching: CrudGetMatching; + crudGetMany: CrudGetMany; + filter?: object; + filterToQuery: (filter: {}) => any; + input?: { + value: any; + }; + matchingReferences?: Record[] | MatchingReferencesError; + meta?: object; + onChange?: () => void; + perPage?: number; + record?: Record; + reference: string; + referenceRecords?: Record[]; + referenceSource: typeof defaultReferenceSource; + resource: string; + sort?: Sort; + source: string; + translate: Translate; +} /** * An Input component for fields containing a list of references to another resource. @@ -98,7 +138,21 @@ const referenceSource = (resource, source) => `${resource}@${source}`; * * */ -export class ReferenceArrayInputController extends Component { +export class ReferenceArrayInputControllerView extends Component { + public static defaultProps = { + allowEmpty: false, + filter: {}, + filterToQuery: searchText => ({ q: searchText }), + matchingReferences: null, + perPage: 25, + sort: { field: 'id', order: 'DESC' }, + referenceRecords: [], + referenceSource: defaultReferenceSource, // used in unit tests + }; + + private params; + private debouncedSetFilter; + constructor(props) { super(props); const { perPage, sort, filter } = props; @@ -108,13 +162,16 @@ export class ReferenceArrayInputController extends Component { } componentDidMount() { - this.fetchReferencesAndOptions(); + this.fetchReferencesAndOptions(this.props); } componentWillReceiveProps(nextProps) { let shouldFetchOptions = false; - if ((this.props.record || {}).id !== (nextProps.record || {}).id) { + if ( + (this.props.record || { id: undefined }).id !== + (nextProps.record || {}).id + ) { this.fetchReferencesAndOptions(nextProps); } else if (this.props.input.value !== nextProps.input.value) { this.fetchReferences(nextProps); @@ -143,21 +200,21 @@ export class ReferenceArrayInputController extends Component { } } - setFilter = filter => { + setFilter = (filter: any) => { if (filter !== this.params.filter) { this.params.filter = this.props.filterToQuery(filter); this.fetchOptions(); } }; - setPagination = pagination => { + setPagination = (pagination: Pagination) => { if (pagination !== this.params.pagination) { this.params.pagination = pagination; this.fetchOptions(); } }; - setSort = sort => { + setSort = (sort: Sort) => { if (sort !== this.params.sort) { this.params.sort = sort; this.fetchOptions(); @@ -231,48 +288,6 @@ export class ReferenceArrayInputController extends Component { } } -ReferenceArrayInputController.propTypes = { - allowEmpty: PropTypes.bool.isRequired, - basePath: PropTypes.string, - children: PropTypes.func.isRequired, - className: PropTypes.string, - crudGetMatching: PropTypes.func.isRequired, - crudGetMany: PropTypes.func.isRequired, - filter: PropTypes.object, - filterToQuery: PropTypes.func.isRequired, - input: PropTypes.object.isRequired, - label: PropTypes.string, - matchingReferences: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.object, - ]), - meta: PropTypes.object, - onChange: PropTypes.func, - perPage: PropTypes.number, - record: PropTypes.object, - reference: PropTypes.string.isRequired, - referenceRecords: PropTypes.array, - referenceSource: PropTypes.func.isRequired, - resource: PropTypes.string.isRequired, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(['ASC', 'DESC']), - }), - source: PropTypes.string, - translate: PropTypes.func.isRequired, -}; - -ReferenceArrayInputController.defaultProps = { - allowEmpty: false, - filter: {}, - filterToQuery: searchText => ({ q: searchText }), - matchingReferences: null, - perPage: 25, - sort: { field: 'id', order: 'DESC' }, - referenceRecords: [], - referenceSource, // used in unit tests -}; - const makeMapStateToProps = () => createSelector( [ @@ -297,8 +312,8 @@ const makeMapStateToProps = () => }) ); -const EnhancedReferenceArrayInputController = compose( - translate, +const ReferenceArrayInputController = compose( + withTranslate, connect( makeMapStateToProps(), { @@ -306,10 +321,10 @@ const EnhancedReferenceArrayInputController = compose( crudGetMatching: crudGetMatchingAction, } ) -)(ReferenceArrayInputController); +)(ReferenceArrayInputControllerView); -EnhancedReferenceArrayInputController.defaultProps = { - referenceSource, // used in makeMapStateToProps +ReferenceArrayInputController.defaultProps = { + referenceSource: defaultReferenceSource, // used in makeMapStateToProps }; -export default EnhancedReferenceArrayInputController; +export default ReferenceArrayInputController; diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.spec.js b/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx similarity index 98% rename from packages/ra-core/src/controller/input/ReferenceInputController.spec.js rename to packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx index a25936f3eff..232a5a92475 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.spec.js +++ b/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx @@ -2,15 +2,17 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; import { render } from 'react-testing-library'; -import { ReferenceInputController } from './ReferenceInputController'; +import { ReferenceInputControllerView as ReferenceInputController } from './ReferenceInputController'; describe('', () => { const defaultProps = { + basePath: '/comments', children: jest.fn(), crudGetManyAccumulate: jest.fn(), crudGetMatchingAccumulate: jest.fn(), meta: {}, - input: {}, + input: { value: undefined }, + onChange: jest.fn(), reference: 'posts', resource: 'comments', source: 'post_id', @@ -114,7 +116,6 @@ describe('', () => { ...defaultProps, matchingReferences: { error: 'fetch error' }, referenceRecord: null, - input: {}, }} > {children} @@ -363,12 +364,7 @@ describe('', () => { input={{ value: 5 }} /> ); - assert.deepEqual(crudGetManyAccumulate.mock.calls[0], [ - 'posts', - [5], - null, - false, - ]); + assert.deepEqual(crudGetManyAccumulate.mock.calls[0], ['posts', [5]]); }); it('should pass onChange down to child component', () => { diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.js b/packages/ra-core/src/controller/input/ReferenceInputController.tsx similarity index 75% rename from packages/ra-core/src/controller/input/ReferenceInputController.js rename to packages/ra-core/src/controller/input/ReferenceInputController.tsx index 1f42c07d0b1..6e5630c5098 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.js +++ b/packages/ra-core/src/controller/input/ReferenceInputController.tsx @@ -1,5 +1,4 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import debounce from 'lodash/debounce'; import compose from 'recompose/compose'; @@ -9,6 +8,8 @@ import isEqual from 'lodash/isEqual'; import { crudGetManyAccumulate as crudGetManyAccumulateAction, crudGetMatchingAccumulate as crudGetMatchingAccumulateAction, + CrudGetMatchingAccumulate, + CrudGetManyAccumulate, } from '../../actions/accumulateActions'; import { getPossibleReferences, @@ -16,9 +17,56 @@ import { getReferenceResource, } from '../../reducer'; import { getStatusForInput as getDataStatus } from './referenceDataStatus'; -import translate from '../../i18n/translate'; +import withTranslate from '../../i18n/translate'; +import { Sort, Translate, Record, Pagination } from '../../types'; +import { MatchingReferencesError } from './types'; -const referenceSource = (resource, source) => `${resource}@${source}`; +const defaultReferenceSource = (resource: string, source: string) => + `${resource}@${source}`; + +interface ChildrenFuncParams { + choices: Record[]; + error?: string; + filter?: any; + isLoading: boolean; + onChange: (value: any) => void; + pagination: Pagination; + setFilter: (filter: any) => void; + setPagination: (pagination: Pagination) => void; + setSort: (sort: Sort) => void; + sort: Sort; + warning?: string; +} + +interface Props { + allowEmpty?: boolean; + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetMatchingAccumulate: CrudGetMatchingAccumulate; + crudGetManyAccumulate: CrudGetManyAccumulate; + filter?: object; + filterToQuery: (filter: {}) => any; + input?: { + value: any; + }; + matchingReferences: Record[] | MatchingReferencesError; + onChange: () => void; + perPage: number; + record?: Record; + reference: string; + referenceRecord?: Record; + referenceSource: typeof defaultReferenceSource; + resource: string; + sort?: Sort; + source: string; + translate: Translate; +} + +interface State { + pagination: Pagination; + sort: Sort; + filter: any; +} /** * An Input component for choosing a reference record. Useful for foreign keys. @@ -99,7 +147,21 @@ const referenceSource = (resource, source) => `${resource}@${source}`; * * */ -export class ReferenceInputController extends Component { +export class ReferenceInputControllerView extends Component { + public static defaultProps = { + allowEmpty: false, + filter: {}, + filterToQuery: searchText => ({ q: searchText }), + matchingReferences: null, + perPage: 25, + sort: { field: 'id', order: 'DESC' }, + referenceRecord: null, + referenceSource: defaultReferenceSource, // used in tests + }; + + public state: State; + private debouncedSetFilter; + constructor(props) { super(props); const { perPage, sort, filter } = props; @@ -112,7 +174,10 @@ export class ReferenceInputController extends Component { } componentWillReceiveProps(nextProps) { - if ((this.props.record || {}).id !== (nextProps.record || {}).id) { + if ( + (this.props.record || { id: undefined }).id !== + (nextProps.record || {}).id + ) { this.fetchReferenceAndOptions(nextProps); } else if (this.props.input.value !== nextProps.input.value) { this.fetchReference(nextProps); @@ -160,7 +225,7 @@ export class ReferenceInputController extends Component { const { crudGetManyAccumulate, input, reference } = props; const id = input.value; if (id) { - crudGetManyAccumulate(reference, [id], null, false); + crudGetManyAccumulate(reference, [id]); } }; @@ -223,47 +288,6 @@ export class ReferenceInputController extends Component { } } -ReferenceInputController.propTypes = { - allowEmpty: PropTypes.bool.isRequired, - basePath: PropTypes.string, - children: PropTypes.func.isRequired, - className: PropTypes.string, - classes: PropTypes.object, - crudGetMatchingAccumulate: PropTypes.func.isRequired, - crudGetManyAccumulate: PropTypes.func.isRequired, - filter: PropTypes.object, - filterToQuery: PropTypes.func.isRequired, - input: PropTypes.object.isRequired, - matchingReferences: PropTypes.oneOfType([ - PropTypes.array, - PropTypes.object, - ]), - onChange: PropTypes.func, - perPage: PropTypes.number, - record: PropTypes.object, - reference: PropTypes.string.isRequired, - referenceRecord: PropTypes.object, - referenceSource: PropTypes.func.isRequired, - resource: PropTypes.string.isRequired, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.oneOf(['ASC', 'DESC']), - }), - source: PropTypes.string, - translate: PropTypes.func.isRequired, -}; - -ReferenceInputController.defaultProps = { - allowEmpty: false, - filter: {}, - filterToQuery: searchText => ({ q: searchText }), - matchingReferences: null, - perPage: 25, - sort: { field: 'id', order: 'DESC' }, - referenceRecord: null, - referenceSource, // used in tests -}; - const makeMapStateToProps = () => createSelector( [ @@ -281,8 +305,8 @@ const makeMapStateToProps = () => }) ); -const EnhancedReferenceInputController = compose( - translate, +const ReferenceInputController = compose( + withTranslate, connect( makeMapStateToProps(), { @@ -290,10 +314,10 @@ const EnhancedReferenceInputController = compose( crudGetMatchingAccumulate: crudGetMatchingAccumulateAction, } ) -)(ReferenceInputController); +)(ReferenceInputControllerView); -EnhancedReferenceInputController.defaultProps = { - referenceSource, // used in makeMapStateToProps +ReferenceInputController.defaultProps = { + referenceSource: defaultReferenceSource, // used in makeMapStateToProps }; -export default EnhancedReferenceInputController; +export default ReferenceInputController; diff --git a/packages/ra-core/src/controller/input/index.js b/packages/ra-core/src/controller/input/index.ts similarity index 100% rename from packages/ra-core/src/controller/input/index.js rename to packages/ra-core/src/controller/input/index.ts diff --git a/packages/ra-core/src/controller/input/referenceDataStatus.spec.js b/packages/ra-core/src/controller/input/referenceDataStatus.spec.ts similarity index 94% rename from packages/ra-core/src/controller/input/referenceDataStatus.spec.js rename to packages/ra-core/src/controller/input/referenceDataStatus.spec.ts index 8c4c95eb100..b76153b9b4b 100644 --- a/packages/ra-core/src/controller/input/referenceDataStatus.spec.js +++ b/packages/ra-core/src/controller/input/referenceDataStatus.spec.ts @@ -18,9 +18,9 @@ describe('References data status', () => { }; it('should indicate whether the data are ready or not', () => { - const test = (data, waiting, explanation) => + const test = (params, waiting, explanation) => assert.equal( - getStatusForInput(data).waiting, + getStatusForInput(params).waiting, waiting, explanation ); @@ -56,8 +56,8 @@ describe('References data status', () => { }); it('should claim an error if needed', () => { - const test = (data, error, explanation) => { - const status = getStatusForInput(data); + const test = (params, error, explanation) => { + const status = getStatusForInput(params); assert.equal(status.waiting, false); assert.equal(status.error, error, explanation); }; @@ -119,8 +119,8 @@ describe('References data status', () => { }); it('should claim a warning if needed', () => { - const test = (data, warning, explanation) => { - const status = getStatusForInput(data); + const test = (params, warning, explanation) => { + const status = getStatusForInput(params); assert.equal(status.waiting, false); assert.equal(status.error, null); assert.equal(status.warning, warning, explanation); @@ -167,8 +167,8 @@ describe('References data status', () => { }); it('should return choices consistent with the data status', () => { - const test = (data, warning, choices, explanation) => { - const status = getStatusForInput(data); + const test = (params, warning, choices, explanation) => { + const status = getStatusForInput(params); assert.equal(status.waiting, false); assert.equal(status.error, null); assert.equal(status.warning, warning); @@ -259,9 +259,9 @@ describe('References data status', () => { }; it('should indicate whether the data are ready or not', () => { - const test = (data, waiting, explanation) => + const test = (params, waiting, explanation) => assert.equal( - getStatusForArrayInput(data).waiting, + getStatusForArrayInput(params).waiting, waiting, explanation ); @@ -301,8 +301,8 @@ describe('References data status', () => { }); it('should return an error if needed', () => { - const test = (data, error, explanation) => { - const status = getStatusForArrayInput(data); + const test = (params, error, explanation) => { + const status = getStatusForArrayInput(params); assert.equal(status.waiting, false); assert.equal(status.error, error, explanation); }; @@ -364,8 +364,8 @@ describe('References data status', () => { }); it('should return a warning if needed', () => { - const test = (data, warning, explanation) => { - const status = getStatusForArrayInput(data); + const test = (params, warning, explanation) => { + const status = getStatusForArrayInput(params); assert.equal(status.waiting, false); assert.equal(status.error, null); assert.equal(status.warning, warning, explanation); @@ -412,8 +412,8 @@ describe('References data status', () => { }); it('should return choices consistent with the data status', () => { - const test = (data, warning, choices, explanation) => { - const status = getStatusForArrayInput(data); + const test = (params, warning, choices, explanation) => { + const status = getStatusForArrayInput(params); assert.equal(status.waiting, false); assert.equal(status.error, null); assert.equal(status.warning, warning); diff --git a/packages/ra-core/src/controller/input/referenceDataStatus.js b/packages/ra-core/src/controller/input/referenceDataStatus.ts similarity index 68% rename from packages/ra-core/src/controller/input/referenceDataStatus.js rename to packages/ra-core/src/controller/input/referenceDataStatus.ts index 354a57b4df1..63d66f84392 100644 --- a/packages/ra-core/src/controller/input/referenceDataStatus.js +++ b/packages/ra-core/src/controller/input/referenceDataStatus.ts @@ -1,15 +1,33 @@ +import { Record, Translate } from '../../types'; +import { MatchingReferencesError } from './types'; + +interface GetStatusForInputParams { + input: { + value: any; + }; + matchingReferences: Record[] | MatchingReferencesError; + referenceRecord: Record; + translate: Translate; +} + +const isMatchingReferencesError = ( + matchingReferences?: any +): matchingReferences is MatchingReferencesError => + matchingReferences && matchingReferences.error !== undefined; + export const getStatusForInput = ({ input, matchingReferences, referenceRecord, translate = x => x, -}) => { - const matchingReferencesError = - matchingReferences && matchingReferences.error - ? translate(matchingReferences.error, { - _: matchingReferences.error, - }) - : null; +}: GetStatusForInputParams) => { + const matchingReferencesError = isMatchingReferencesError( + matchingReferences + ) + ? translate(matchingReferences.error, { + _: matchingReferences.error, + }) + : null; const selectedReferenceError = input.value && !referenceRecord ? translate('ra.input.references.single_missing', { @@ -41,19 +59,33 @@ export const REFERENCES_STATUS_READY = 'REFERENCES_STATUS_READY'; export const REFERENCES_STATUS_INCOMPLETE = 'REFERENCES_STATUS_INCOMPLETE'; export const REFERENCES_STATUS_EMPTY = 'REFERENCES_STATUS_EMPTY'; -export const getSelectedReferencesStatus = (input, referenceRecords) => +export const getSelectedReferencesStatus = ( + input: { + value: any; + }, + referenceRecords: Record[] +) => !input.value || input.value.length === referenceRecords.length ? REFERENCES_STATUS_READY : referenceRecords.length > 0 - ? REFERENCES_STATUS_INCOMPLETE - : REFERENCES_STATUS_EMPTY; + ? REFERENCES_STATUS_INCOMPLETE + : REFERENCES_STATUS_EMPTY; + +interface GetStatusForArrayInputParams { + input: { + value: any; + }; + matchingReferences: Record[] | MatchingReferencesError; + referenceRecords: Record[]; + translate: Translate; +} export const getStatusForArrayInput = ({ input, matchingReferences, referenceRecords, translate = x => x, -}) => { +}: GetStatusForArrayInputParams) => { // selectedReferencesData can be "empty" (no data was found for references from input.value) // or "incomplete" (Not all of the reference data was found) // or "ready" (all references data was found or there is no references from input.value) @@ -62,12 +94,13 @@ export const getStatusForArrayInput = ({ referenceRecords ); - const matchingReferencesError = - matchingReferences && matchingReferences.error - ? translate(matchingReferences.error, { - _: matchingReferences.error, - }) - : null; + const matchingReferencesError = isMatchingReferencesError( + matchingReferences + ) + ? translate(matchingReferences.error, { + _: matchingReferences.error, + }) + : null; return { waiting: diff --git a/packages/ra-core/src/controller/input/types.ts b/packages/ra-core/src/controller/input/types.ts new file mode 100644 index 00000000000..c4527380417 --- /dev/null +++ b/packages/ra-core/src/controller/input/types.ts @@ -0,0 +1,3 @@ +export interface MatchingReferencesError { + error: string; +} From d0fab3789517163a18e9f0f3531c5d34b8f02441 Mon Sep 17 00:00:00 2001 From: adi Date: Wed, 13 Feb 2019 18:21:13 +0200 Subject: [PATCH 15/35] Fix typo Changed "if" to "of" at lines: 803, 860. --- docs/Tutorial.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 627f4272a10..b379dfeeab4 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -800,7 +800,7 @@ import { stringify } from 'query-string'; const API_URL = 'my.api.url'; /** - * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' + * @param {String} type One of the constants appearing at the top of this file, e.g. 'UPDATE' * @param {String} resource Name of the resource to fetch, e.g. 'posts' * @param {Object} params The Data Provider request params, depending on the type * @returns {Object} { url, options } The HTTP request parameters @@ -857,7 +857,7 @@ const convertDataProviderRequestToHTTP = (type, resource, params) => { /** * @param {Object} response HTTP response from fetch() - * @param {String} type One of the constants appearing at the top if this file, e.g. 'UPDATE' + * @param {String} type One of the constants appearing at the top of this file, e.g. 'UPDATE' * @param {String} resource Name of the resource to fetch, e.g. 'posts' * @param {Object} params The Data Provider request params, depending on the type * @returns {Object} Data Provider response From 7a629a1d1ddf3e40e718cedf2a425c742ba6965f Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 13 Feb 2019 17:44:58 +0100 Subject: [PATCH 16/35] Views --- .../src/actions/dataActions/crudCreate.ts | 7 + .../src/actions/dataActions/crudGetList.ts | 7 + .../src/actions/dataActions/crudGetOne.ts | 7 + .../src/actions/dataActions/crudUpdate.ts | 9 + packages/ra-core/src/actions/listActions.ts | 12 +- packages/ra-core/src/actions/undoActions.ts | 2 + ...ller.spec.js => CreateController.spec.tsx} | 29 ++- ...eateController.js => CreateController.tsx} | 90 +++++--- .../{EditController.js => EditController.tsx} | 84 +++++--- ...roller.spec.js => ListController.spec.tsx} | 38 ++-- .../{ListController.js => ListController.tsx} | 198 +++++++++++------- .../{ShowController.js => ShowController.tsx} | 63 +++--- ...Props.js => checkMinimumRequiredProps.tsx} | 8 +- .../src/controller/{index.js => index.ts} | 0 .../controller/input/referenceDataStatus.ts | 4 +- .../admin/resource/list/queryReducer.ts | 14 +- 16 files changed, 364 insertions(+), 208 deletions(-) rename packages/ra-core/src/controller/{CreateController.spec.js => CreateController.spec.tsx} (72%) rename packages/ra-core/src/controller/{CreateController.js => CreateController.tsx} (65%) rename packages/ra-core/src/controller/{EditController.js => EditController.tsx} (75%) rename packages/ra-core/src/controller/{ListController.spec.js => ListController.spec.tsx} (83%) rename packages/ra-core/src/controller/{ListController.js => ListController.tsx} (79%) rename packages/ra-core/src/controller/{ShowController.js => ShowController.tsx} (75%) rename packages/ra-core/src/controller/{checkMinimumRequiredProps.js => checkMinimumRequiredProps.tsx} (80%) rename packages/ra-core/src/controller/{index.js => index.ts} (100%) diff --git a/packages/ra-core/src/actions/dataActions/crudCreate.ts b/packages/ra-core/src/actions/dataActions/crudCreate.ts index 93b853d2177..e342ac848e2 100644 --- a/packages/ra-core/src/actions/dataActions/crudCreate.ts +++ b/packages/ra-core/src/actions/dataActions/crudCreate.ts @@ -6,6 +6,13 @@ import { RedirectionSideEffect, } from '../../sideEffect'; +export type CrudCreate = ( + resource: string, + data: any, + basePath: string, + redirectTo: RedirectionSideEffect +) => void; + export const crudCreate = ( resource: string, data: any, diff --git a/packages/ra-core/src/actions/dataActions/crudGetList.ts b/packages/ra-core/src/actions/dataActions/crudGetList.ts index b6a256f48de..e9ef3009fba 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetList.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetList.ts @@ -3,6 +3,13 @@ import { GET_LIST } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; +export type CrudGetList = ( + resource: string, + pagination: Pagination, + sort: Sort, + filter: object +) => void; + export const crudGetList = ( resource: string, pagination: Pagination, diff --git a/packages/ra-core/src/actions/dataActions/crudGetOne.ts b/packages/ra-core/src/actions/dataActions/crudGetOne.ts index 295ab3334a3..54d4a539e3c 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetOne.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetOne.ts @@ -7,6 +7,13 @@ import { RefreshSideEffect, } from '../../sideEffect'; +export type CrudGetOne = ( + resource: string, + id: Identifier, + basePath: string, + refresh?: RefreshSideEffect +) => void; + export const crudGetOne = ( resource: string, id: Identifier, diff --git a/packages/ra-core/src/actions/dataActions/crudUpdate.ts b/packages/ra-core/src/actions/dataActions/crudUpdate.ts index 5f470039cd5..7b7fc99cd55 100644 --- a/packages/ra-core/src/actions/dataActions/crudUpdate.ts +++ b/packages/ra-core/src/actions/dataActions/crudUpdate.ts @@ -6,6 +6,15 @@ import { RedirectionSideEffect, } from '../../sideEffect'; +export type CrudUpdate = ( + resource: string, + id: Identifier, + data: any, + previousData: any, + basePath: string, + redirectTo: RedirectionSideEffect +) => void; + export const crudUpdate = ( resource: string, id: Identifier, diff --git a/packages/ra-core/src/actions/listActions.ts b/packages/ra-core/src/actions/listActions.ts index 9eacc4dc0a3..daf0139e694 100644 --- a/packages/ra-core/src/actions/listActions.ts +++ b/packages/ra-core/src/actions/listActions.ts @@ -1,6 +1,6 @@ export const CRUD_CHANGE_LIST_PARAMS = 'RA/CRUD_CHANGE_LIST_PARAMS'; -interface Params { +export interface ListParams { sort: string; order: string; page: number; @@ -10,12 +10,18 @@ interface Params { export interface ChangeListParamsAction { readonly type: typeof CRUD_CHANGE_LIST_PARAMS; - readonly payload: Params; + readonly payload: ListParams; readonly meta: { resource: string }; } + +export type ChangeListParams = ( + resource: string, + params: Partial +) => void; + export const changeListParams = ( resource: string, - params: Params + params: ListParams ): ChangeListParamsAction => ({ type: CRUD_CHANGE_LIST_PARAMS, payload: params, diff --git a/packages/ra-core/src/actions/undoActions.ts b/packages/ra-core/src/actions/undoActions.ts index fd6da504c2e..12a150975ba 100644 --- a/packages/ra-core/src/actions/undoActions.ts +++ b/packages/ra-core/src/actions/undoActions.ts @@ -5,6 +5,8 @@ export interface StartUndoableAction { readonly payload: any; } +export type StartUndoable = (action: any) => void; + export const startUndoable = (action: any): StartUndoableAction => ({ type: UNDOABLE, payload: { action }, diff --git a/packages/ra-core/src/controller/CreateController.spec.js b/packages/ra-core/src/controller/CreateController.spec.tsx similarity index 72% rename from packages/ra-core/src/controller/CreateController.spec.js rename to packages/ra-core/src/controller/CreateController.spec.tsx index e3e8f35d729..b37e6823d82 100644 --- a/packages/ra-core/src/controller/CreateController.spec.js +++ b/packages/ra-core/src/controller/CreateController.spec.tsx @@ -1,17 +1,27 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { CreateController } from './CreateController'; +import { CreateControllerView as CreateController } from './CreateController'; describe('CreateController', () => { describe('Presetting the record from the location', () => { const defaultProps = { basePath: '', - crudCreate: () => {}, + crudCreate: jest.fn(), + hasCreate: true, + hasEdit: true, + hasList: true, + hasShow: true, isLoading: false, - location: {}, - match: {}, + location: { + pathname: '/foo', + search: undefined, + state: undefined, + hash: undefined, + }, + match: { isExact: true, path: '/foo', params: undefined, url: '' }, resource: 'foo', + title: 'Foo', translate: x => x, }; @@ -33,7 +43,10 @@ describe('CreateController', () => { const props = { ...defaultProps, children: childrenMock, - location: { state: { record: { foo: 'bar' } } }, + location: { + ...defaultProps.location, + state: { record: { foo: 'bar' } }, + }, }; shallow(); @@ -47,7 +60,10 @@ describe('CreateController', () => { const props = { ...defaultProps, children: childrenMock, - location: { search: '?foo=baz&array[]=1&array[]=2' }, + location: { + ...defaultProps.location, + search: '?foo=baz&array[]=1&array[]=2', + }, }; shallow(); @@ -64,6 +80,7 @@ describe('CreateController', () => { ...defaultProps, children: childrenMock, location: { + ...defaultProps.location, state: { record: { foo: 'bar' } }, search: '?foo=baz', }, diff --git a/packages/ra-core/src/controller/CreateController.js b/packages/ra-core/src/controller/CreateController.tsx similarity index 65% rename from packages/ra-core/src/controller/CreateController.js rename to packages/ra-core/src/controller/CreateController.tsx index ddf9c1d02ce..98a8f8494f0 100644 --- a/packages/ra-core/src/controller/CreateController.js +++ b/packages/ra-core/src/controller/CreateController.tsx @@ -1,13 +1,44 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; import inflection from 'inflection'; import { parse } from 'query-string'; -import translate from '../i18n/translate'; -import { crudCreate as crudCreateAction } from '../actions'; +import withTranslate from '../i18n/translate'; +import { crudCreate as crudCreateAction, CrudCreate } from '../actions'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; +import { Location } from 'history'; +import { match as Match } from 'react-router'; +import { Record, Translate } from '../types'; +import { RedirectionSideEffect } from '../sideEffect'; + +interface ChildrenFuncParams { + isLoading: boolean; + defaultTitle: string; + save: (record: Partial, redirect: RedirectionSideEffect) => void; + resource: string; + basePath: string; + record?: Record; + redirect: RedirectionSideEffect; + translate: Translate; +} + +interface Props { + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudCreate: CrudCreate; + hasCreate: boolean; + hasEdit: boolean; + hasList: boolean; + hasShow: boolean; + isLoading: boolean; + location: Location; + match: Match; + record?: Record; + resource: string; + title: string | ReactNode; + translate: Translate; +} /** * Page component for the Create view @@ -50,7 +81,13 @@ import checkMinimumRequiredProps from './checkMinimumRequiredProps'; * ); * export default App; */ -export class CreateController extends Component { +export class CreateControllerView extends Component { + public static defaultProps = { + record: {}, + }; + + private record; + constructor(props) { super(props); const { @@ -67,12 +104,16 @@ export class CreateController extends Component { defaultRedirectRoute() { const { hasShow, hasEdit } = this.props; - if (hasEdit) return 'edit'; - if (hasShow) return 'show'; + if (hasEdit) { + return 'edit'; + } + if (hasShow) { + return 'show'; + } return 'list'; } - save = (record, redirect) => { + save = (record: Partial, redirect: RedirectionSideEffect) => { this.props.crudCreate( this.props.resource, record, @@ -90,7 +131,9 @@ export class CreateController extends Component { translate, } = this.props; - if (!children) return null; + if (!children) { + return null; + } const resourceName = translate(`resources.${resource}.name`, { smart_count: 1, @@ -112,38 +155,19 @@ export class CreateController extends Component { } } -CreateController.propTypes = { - basePath: PropTypes.string.isRequired, - children: PropTypes.func.isRequired, - crudCreate: PropTypes.func.isRequired, - hasCreate: PropTypes.bool, - hasEdit: PropTypes.bool, - hasList: PropTypes.bool, - hasShow: PropTypes.bool, - isLoading: PropTypes.bool.isRequired, - location: PropTypes.object.isRequired, - match: PropTypes.object.isRequired, - record: PropTypes.object, - resource: PropTypes.string.isRequired, - title: PropTypes.any, - translate: PropTypes.func.isRequired, -}; - -CreateController.defaultProps = { - record: {}, -}; - function mapStateToProps(state) { return { isLoading: state.admin.loading > 0, }; } -export default compose( +const CreateController = compose( checkMinimumRequiredProps('Create', ['basePath', 'location', 'resource']), connect( mapStateToProps, { crudCreate: crudCreateAction } ), - translate -)(CreateController); + withTranslate +)(CreateControllerView); + +export default CreateController; diff --git a/packages/ra-core/src/controller/EditController.js b/packages/ra-core/src/controller/EditController.tsx similarity index 75% rename from packages/ra-core/src/controller/EditController.js rename to packages/ra-core/src/controller/EditController.tsx index f5a2ce1ced6..eddc37f5ad3 100644 --- a/packages/ra-core/src/controller/EditController.js +++ b/packages/ra-core/src/controller/EditController.tsx @@ -1,13 +1,56 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; import inflection from 'inflection'; import { reset } from 'redux-form'; -import translate from '../i18n/translate'; -import { crudGetOne, crudUpdate, startUndoable } from '../actions'; +import withTranslate from '../i18n/translate'; +import { + crudGetOne, + crudUpdate, + startUndoable as startUndoableAction, + CrudGetOne, + CrudUpdate, + StartUndoable, +} from '../actions'; import { REDUX_FORM_NAME } from '../form'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; +import { Translate, Record } from '../types'; +import { RedirectionSideEffect } from '../sideEffect'; + +interface ChildrenFuncParams { + isLoading: boolean; + defaultTitle: string; + save: (data: Record, redirect: RedirectionSideEffect) => void; + resource: string; + basePath: string; + record?: Record; + redirect: RedirectionSideEffect; + translate: Translate; + version: number; +} + +interface Props { + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetOne: CrudGetOne; + dispatchCrudUpdate: CrudUpdate; + record?: Record; + hasCreate: boolean; + hasEdit: boolean; + hasShow: boolean; + hasList: boolean; + id: string; + isLoading: boolean; + location: object; + match: object; + resetForm: (form: string) => void; + resource: string; + startUndoable: StartUndoable; + title: string | ReactNode; + translate: Translate; + undoable?: boolean; + version: number; +} /** * Page component for the Edit view @@ -51,7 +94,7 @@ import checkMinimumRequiredProps from './checkMinimumRequiredProps'; * ); * export default App; */ -export class EditController extends Component { +export class EditController extends Component { componentDidMount() { this.updateData(); } @@ -115,7 +158,9 @@ export class EditController extends Component { version, } = this.props; - if (!children) return null; + if (!children) { + return null; + } const resourceName = translate(`resources.${resource}.name`, { smart_count: 1, @@ -141,29 +186,6 @@ export class EditController extends Component { } } -EditController.propTypes = { - basePath: PropTypes.string.isRequired, - children: PropTypes.func.isRequired, - crudGetOne: PropTypes.func.isRequired, - dispatchCrudUpdate: PropTypes.func.isRequired, - record: PropTypes.object, - hasCreate: PropTypes.bool, - hasEdit: PropTypes.bool, - hasShow: PropTypes.bool, - hasList: PropTypes.bool, - id: PropTypes.string.isRequired, - isLoading: PropTypes.bool.isRequired, - location: PropTypes.object, - match: PropTypes.object, - resetForm: PropTypes.func.isRequired, - resource: PropTypes.string.isRequired, - startUndoable: PropTypes.func.isRequired, - title: PropTypes.any, - translate: PropTypes.func, - undoable: PropTypes.bool, - version: PropTypes.number.isRequired, -}; - function mapStateToProps(state, props) { return { id: props.id, @@ -182,9 +204,9 @@ export default compose( { crudGetOne, dispatchCrudUpdate: crudUpdate, - startUndoable, + startUndoable: startUndoableAction, resetForm: reset, } ), - translate + withTranslate )(EditController); diff --git a/packages/ra-core/src/controller/ListController.spec.js b/packages/ra-core/src/controller/ListController.spec.tsx similarity index 83% rename from packages/ra-core/src/controller/ListController.spec.js rename to packages/ra-core/src/controller/ListController.spec.tsx index 26f45994433..8276c7e94f9 100644 --- a/packages/ra-core/src/controller/ListController.spec.js +++ b/packages/ra-core/src/controller/ListController.spec.tsx @@ -1,10 +1,9 @@ import React from 'react'; import { shallow } from 'enzyme'; import lolex from 'lolex'; -import { setDisplayName } from 'recompose'; import { - ListController, + ListControllerView as ListController, getListControllerProps, sanitizeListRestProps, } from './ListController'; @@ -13,9 +12,9 @@ import TextField from '@material-ui/core/TextField/TextField'; describe('ListController', () => { const defaultProps = { basePath: '', - changeListParams: () => {}, - children: () => {}, - crudGetList: () => {}, + changeListParams: jest.fn(), + children: jest.fn(), + crudGetList: jest.fn(), hasCreate: true, hasEdit: true, hasList: true, @@ -23,16 +22,25 @@ describe('ListController', () => { ids: [], isLoading: false, location: { - pathname: '', + pathname: '/foo', + search: undefined, + state: undefined, + hash: undefined, }, - params: { filter: {} }, - push: () => {}, + params: { + filter: undefined, + perPage: undefined, + page: undefined, + order: undefined, + sort: undefined, + }, + push: jest.fn(), query: {}, resource: '', - setSelectedIds: () => {}, - toggleItem: () => {}, + setSelectedIds: jest.fn(), + toggleItem: jest.fn(), total: 100, - translate: () => {}, + translate: jest.fn(), }; describe('setFilters', () => { @@ -41,9 +49,9 @@ describe('ListController', () => { beforeEach(() => { clock = lolex.install(); - fakeComponent = setDisplayName( - 'FakeComponent' // for eslint - )(({ setFilters }) => ); + fakeComponent = ({ setFilters }) => ( + + ); }); it('should take only last change in case of a burst of changes (case of inputs being currently edited)', () => { @@ -74,7 +82,7 @@ describe('ListController', () => { ...defaultProps, debounce: 200, changeListParams: jest.fn(), - params: { filter: { q: 'hello' } }, + params: { ...defaultProps.params, filter: { q: 'hello' } }, children: fakeComponent, }; diff --git a/packages/ra-core/src/controller/ListController.js b/packages/ra-core/src/controller/ListController.tsx similarity index 79% rename from packages/ra-core/src/controller/ListController.js rename to packages/ra-core/src/controller/ListController.tsx index 400fda16074..872280d3374 100644 --- a/packages/ra-core/src/controller/ListController.js +++ b/packages/ra-core/src/controller/ListController.tsx @@ -1,6 +1,5 @@ /* eslint no-console: ["error", { allow: ["warn", "error"] }] */ -import { Component, isValidElement } from 'react'; -import PropTypes from 'prop-types'; +import { Component, isValidElement, ReactNode, ReactElement } from 'react'; import { connect } from 'react-redux'; import { parse, stringify } from 'query-string'; import { push as pushAction } from 'react-router-redux'; @@ -19,15 +18,88 @@ import queryReducer, { SET_FILTER, SORT_DESC, } from '../reducer/admin/resource/list/queryReducer'; -import { crudGetList as crudGetListAction } from '../actions/dataActions'; +import { + crudGetList as crudGetListAction, + CrudGetList, +} from '../actions/dataActions'; import { changeListParams as changeListParamsAction, setListSelectedIds as setListSelectedIdsAction, toggleListItem as toggleListItemAction, + ListParams, + ChangeListParams, } from '../actions/listActions'; -import translate from '../i18n/translate'; +import withTranslate from '../i18n/translate'; import removeKey from '../util/removeKey'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; +import { Sort, AuthProvider, RecordMap, Identifier, Translate } from '../types'; +import { Location, LocationDescriptorObject, LocationState } from 'history'; + +interface ChildrenFuncParams { + basePath: string; + currentSort: Sort; + data: RecordMap; + defaultTitle: string; + displayedFilters: any; + filterValues: any; + hasCreate: boolean; + hideFilter: (filterName: string) => void; + ids: Identifier[]; + isLoading: boolean; + loadedOnce: boolean; + onSelect: (ids: Identifier[]) => void; + onToggleItem: (id: Identifier) => void; + onUnselectItems: () => void; + page: number; + perPage: number; + resource: string; + selectedIds: Identifier[]; + setFilters: (filters: any) => void; + setPage: (page: number) => void; + setPerPage: (page: number) => void; + setSort: (sort: Sort) => void; + showFilter: (filterName: string, defaultValue: any) => void; + translate: Translate; + total: number; + version: number; +} + +interface Props { + // the props you can change + children: (params: ChildrenFuncParams) => ReactNode; + filter?: object; + filters: ReactElement; + filterDefaultValues: object; + pagination: ReactElement; + perPage: number; + sort: Sort; + // the props managed by react-admin + authProvider: AuthProvider; + basePath: string; + changeListParams: ChangeListParams; + crudGetList: CrudGetList; + data?: RecordMap; + debounce: number; + hasCreate: boolean; + hasEdit: boolean; + hasList: boolean; + hasShow: boolean; + ids: Identifier[]; + loadedOnce: boolean; + selectedIds: Identifier[]; + isLoading: boolean; + location: Location; + path: string; + params: ListParams; + push: (location: LocationDescriptorObject) => void; + query: Partial; + resource: string; + setSelectedIds: (resource: string, ids: Identifier[]) => void; + toggleItem: (resource: string, id: Identifier) => void; + total: number; + translate: Translate; + version: number; +} /** * List page component @@ -70,9 +142,29 @@ import checkMinimumRequiredProps from './checkMinimumRequiredProps'; * * ); */ -export class ListController extends Component { +export class ListControllerView extends Component { + public static defaultProps: Partial = { + debounce: 500, + filter: {}, + perPage: 10, + sort: { + field: 'id', + order: SORT_DESC, + }, + }; + state = {}; + setFilters = debounce(filters => { + if (isEqual(filters, this.getFilterValues())) { + return; + } + + // fix for redux-form bug with onChange and enableReinitialize + const filtersWithoutEmpty = removeEmpty(filters); + this.changeParams({ type: SET_FILTER, payload: filtersWithoutEmpty }); + }, this.props.debounce); + componentDidMount() { if (this.props.filter && isValidElement(this.props.filter)) { throw new Error( @@ -99,7 +191,7 @@ export class ListController extends Component { this.setFilters.cancel(); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { if ( nextProps.resource !== this.props.resource || nextProps.query.sort !== this.props.query.sort || @@ -122,7 +214,7 @@ export class ListController extends Component { } } - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: Props, nextState) { if ( nextProps.translate === this.props.translate && nextProps.isLoading === this.props.isLoading && @@ -150,7 +242,7 @@ export class ListController extends Component { * * @param {object} params */ - hasCustomParams(params) { + hasCustomParams(params: ListParams) { return ( params && params.filter && @@ -170,7 +262,7 @@ export class ListController extends Component { * - the props passed to the List component */ getQuery() { - const query = + const query: Partial = Object.keys(this.props.query).length > 0 ? this.props.query : this.hasCustomParams(this.props.params) @@ -195,7 +287,7 @@ export class ListController extends Component { return query.filter || {}; } - updateData(query) { + updateData(query?: any) { const params = query || this.getQuery(); const { sort, order, page = 1, perPage, filter } = params; const pagination = { @@ -218,17 +310,7 @@ export class ListController extends Component { setPerPage = perPage => this.changeParams({ type: SET_PER_PAGE, payload: perPage }); - setFilters = debounce(filters => { - if (isEqual(filters, this.getFilterValues())) { - return; - } - - // fix for redux-form bug with onChange and enableReinitialize - const filtersWithoutEmpty = removeEmpty(filters); - this.changeParams({ type: SET_FILTER, payload: filtersWithoutEmpty }); - }, this.props.debounce); - - showFilter = (filterName, defaultValue) => { + showFilter = (filterName: string, defaultValue: any) => { this.setState({ [filterName]: true }); if (typeof defaultValue !== 'undefined') { this.setFilters({ @@ -238,13 +320,13 @@ export class ListController extends Component { } }; - hideFilter = filterName => { + hideFilter = (filterName: string) => { this.setState({ [filterName]: false }); const newFilters = removeKey(this.getFilterValues(), filterName); this.setFilters(newFilters); }; - handleSelect = ids => { + handleSelect = (ids: Identifier[]) => { this.props.setSelectedIds(this.props.resource, ids); }; @@ -252,7 +334,7 @@ export class ListController extends Component { this.props.setSelectedIds(this.props.resource, []); }; - handleToggleItem = id => { + handleToggleItem = (id: Identifier) => { this.props.toggleItem(this.props.resource, id); }; @@ -310,9 +392,11 @@ export class ListController extends Component { onSelect: this.handleSelect, onToggleItem: this.handleToggleItem, onUnselectItems: this.handleUnselectItems, - page: parseInt(query.page || 1, 10), - perPage: parseInt(query.perPage, 10), - refresh: this.refresh, + page: + (typeof query.page === 'string' + ? parseInt(query.page, 10) + : query.page) || 1, + perPage: parseInt(query.perPage.toString(), 10), resource, selectedIds, setFilters: this.setFilters, @@ -327,56 +411,6 @@ export class ListController extends Component { } } -ListController.propTypes = { - // the props you can change - children: PropTypes.func.isRequired, - filter: PropTypes.object, - filters: PropTypes.element, - filterDefaultValues: PropTypes.object, // eslint-disable-line react/forbid-prop-types - pagination: PropTypes.element, - perPage: PropTypes.number.isRequired, - sort: PropTypes.shape({ - field: PropTypes.string, - order: PropTypes.string, - }), - // the props managed by react-admin - authProvider: PropTypes.func, - basePath: PropTypes.string.isRequired, - changeListParams: PropTypes.func.isRequired, - crudGetList: PropTypes.func.isRequired, - data: PropTypes.object, // eslint-disable-line react/forbid-prop-types - debounce: PropTypes.number, - hasCreate: PropTypes.bool, - hasEdit: PropTypes.bool, - hasList: PropTypes.bool, - hasShow: PropTypes.bool, - ids: PropTypes.array, - loadedOnce: PropTypes.bool, - selectedIds: PropTypes.array, - isLoading: PropTypes.bool.isRequired, - location: PropTypes.object.isRequired, - path: PropTypes.string, - params: PropTypes.object.isRequired, - push: PropTypes.func.isRequired, - query: PropTypes.object.isRequired, - resource: PropTypes.string.isRequired, - setSelectedIds: PropTypes.func.isRequired, - toggleItem: PropTypes.func.isRequired, - total: PropTypes.number.isRequired, - translate: PropTypes.func.isRequired, - version: PropTypes.number, -}; - -ListController.defaultProps = { - debounce: 500, - filter: {}, - perPage: 10, - sort: { - field: 'id', - order: SORT_DESC, - }, -}; - const injectedProps = [ 'basePath', 'currentSort', @@ -422,7 +456,7 @@ export const getListControllerProps = props => */ export const sanitizeListRestProps = props => Object.keys(props) - .filter(props => !injectedProps.includes(props)) + .filter(propName => !injectedProps.includes(propName)) .reduce((acc, key) => ({ ...acc, [key]: props[key] }), {}); const validQueryParams = ['page', 'perPage', 'sort', 'order', 'filter']; @@ -463,7 +497,7 @@ function mapStateToProps(state, props) { }; } -export default compose( +const ListController = compose( checkMinimumRequiredProps('List', ['basePath', 'location', 'resource']), connect( mapStateToProps, @@ -475,5 +509,7 @@ export default compose( push: pushAction, } ), - translate -)(ListController); + withTranslate +)(ListControllerView); + +export default ListController; diff --git a/packages/ra-core/src/controller/ShowController.js b/packages/ra-core/src/controller/ShowController.tsx similarity index 75% rename from packages/ra-core/src/controller/ShowController.js rename to packages/ra-core/src/controller/ShowController.tsx index a866a266353..272bef151fb 100644 --- a/packages/ra-core/src/controller/ShowController.js +++ b/packages/ra-core/src/controller/ShowController.tsx @@ -1,11 +1,41 @@ -import { Component } from 'react'; -import PropTypes from 'prop-types'; +import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import compose from 'recompose/compose'; import inflection from 'inflection'; -import translate from '../i18n/translate'; -import { crudGetOne as crudGetOneAction } from '../actions'; +import withTranslate from '../i18n/translate'; +import { crudGetOne as crudGetOneAction, CrudGetOne } from '../actions'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; +import { Translate, Record } from '../types'; + +interface ChildrenFuncParams { + isLoading: boolean; + defaultTitle: string; + resource: string; + basePath: string; + record?: Record; + title: string | ReactNode; + translate: Translate; + version: number; +} + +interface Props { + basePath: string; + children: (params: ChildrenFuncParams) => ReactNode; + crudGetOne: CrudGetOne; + record?: Record; + hasCreate: boolean; + hasEdit: boolean; + hasShow: boolean; + hasList: boolean; + id: string; + isLoading: boolean; + location: object; + match: object; + resource: string; + title: string | ReactNode; + translate: Translate; + version: number; +} /** * Page component for the Show view @@ -49,7 +79,7 @@ import checkMinimumRequiredProps from './checkMinimumRequiredProps'; * ); * export default App; */ -export class ShowController extends Component { +export class ShowController extends Component { componentDidMount() { this.updateData(); } @@ -80,7 +110,9 @@ export class ShowController extends Component { version, } = this.props; - if (!children) return null; + if (!children) { + return null; + } const resourceName = translate(`resources.${resource}.name`, { smart_count: 1, @@ -104,23 +136,6 @@ export class ShowController extends Component { } } -ShowController.propTypes = { - basePath: PropTypes.string.isRequired, - children: PropTypes.func.isRequired, - crudGetOne: PropTypes.func.isRequired, - record: PropTypes.object, - hasCreate: PropTypes.bool, - hasEdit: PropTypes.bool, - hasList: PropTypes.bool, - hasShow: PropTypes.bool, - id: PropTypes.string.isRequired, - isLoading: PropTypes.bool.isRequired, - resource: PropTypes.string.isRequired, - title: PropTypes.any, - translate: PropTypes.func, - version: PropTypes.number.isRequired, -}; - function mapStateToProps(state, props) { return { id: props.id, @@ -138,5 +153,5 @@ export default compose( mapStateToProps, { crudGetOne: crudGetOneAction } ), - translate + withTranslate )(ShowController); diff --git a/packages/ra-core/src/controller/checkMinimumRequiredProps.js b/packages/ra-core/src/controller/checkMinimumRequiredProps.tsx similarity index 80% rename from packages/ra-core/src/controller/checkMinimumRequiredProps.js rename to packages/ra-core/src/controller/checkMinimumRequiredProps.tsx index e3fb24c75a3..6232846dadd 100644 --- a/packages/ra-core/src/controller/checkMinimumRequiredProps.js +++ b/packages/ra-core/src/controller/checkMinimumRequiredProps.tsx @@ -1,9 +1,9 @@ -import React from 'react'; +import React, { ComponentType } from 'react'; const checkMinimumRequiredProps = ( - displayName, - requiredProps -) => WrappedComponent => props => { + displayName: string, + requiredProps: string[] +) => (WrappedComponent: ComponentType) => (props: any) => { const propNames = Object.keys(props); const missingProps = requiredProps.filter( prop => !propNames.includes(prop) diff --git a/packages/ra-core/src/controller/index.js b/packages/ra-core/src/controller/index.ts similarity index 100% rename from packages/ra-core/src/controller/index.js rename to packages/ra-core/src/controller/index.ts diff --git a/packages/ra-core/src/controller/input/referenceDataStatus.ts b/packages/ra-core/src/controller/input/referenceDataStatus.ts index 63d66f84392..b58431bf310 100644 --- a/packages/ra-core/src/controller/input/referenceDataStatus.ts +++ b/packages/ra-core/src/controller/input/referenceDataStatus.ts @@ -68,8 +68,8 @@ export const getSelectedReferencesStatus = ( !input.value || input.value.length === referenceRecords.length ? REFERENCES_STATUS_READY : referenceRecords.length > 0 - ? REFERENCES_STATUS_INCOMPLETE - : REFERENCES_STATUS_EMPTY; + ? REFERENCES_STATUS_INCOMPLETE + : REFERENCES_STATUS_EMPTY; interface GetStatusForArrayInputParams { input: { diff --git a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts index 89efa4c7ec8..d860f6f25ae 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts @@ -1,4 +1,5 @@ import { Reducer } from 'redux'; +import { ListParams } from '../../../../actions'; export const SET_SORT = 'SET_SORT'; export const SORT_ASC = 'ASC'; export const SORT_DESC = 'DESC'; @@ -11,18 +12,13 @@ export const SET_FILTER = 'SET_FILTER'; const oppositeOrder = direction => direction === SORT_DESC ? SORT_ASC : SORT_DESC; -interface State { - sort?: string; - order?: string; - page?: number; - perPage?: number; - filter?: any; -} - /** * This reducer is for the react-router query string, NOT for redux. */ -const queryReducer: Reducer = (previousState, { type, payload }) => { +const queryReducer: Reducer> = ( + previousState, + { type, payload } +) => { switch (type) { case SET_SORT: if (payload === previousState.sort) { From af66dbb978f03b66e362edbc25659fafbc6a235f Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Thu, 14 Feb 2019 10:52:32 +0100 Subject: [PATCH 17/35] Better typing for dispatchers --- package.json | 10 ++-- .../ra-core/src/actions/accumulateActions.ts | 11 ---- .../src/actions/dataActions/crudCreate.ts | 7 --- .../src/actions/dataActions/crudGetList.ts | 7 --- .../src/actions/dataActions/crudGetMany.ts | 2 - .../dataActions/crudGetManyReference.ts | 11 ---- .../actions/dataActions/crudGetMatching.ts | 8 --- .../src/actions/dataActions/crudGetOne.ts | 7 --- .../src/actions/dataActions/crudUpdate.ts | 9 --- packages/ra-core/src/actions/listActions.ts | 5 -- packages/ra-core/src/actions/undoActions.ts | 2 - .../src/controller/CreateController.tsx | 6 +- .../ra-core/src/controller/EditController.tsx | 11 ++-- .../ra-core/src/controller/ListController.tsx | 19 +++--- .../ra-core/src/controller/ShowController.tsx | 6 +- .../field/ReferenceArrayFieldController.tsx | 9 +-- .../field/ReferenceFieldController.tsx | 9 +-- .../field/ReferenceManyFieldController.tsx | 9 +-- .../input/ReferenceArrayInputController.tsx | 8 +-- .../input/ReferenceInputController.tsx | 8 +-- packages/ra-core/src/types.ts | 4 ++ yarn.lock | 59 ++++++++++++------- 22 files changed, 84 insertions(+), 143 deletions(-) diff --git a/package.json b/package.json index c8887377fdf..548f7a8bb0f 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,10 @@ "lolex": "~2.3.2", "prettier": "~1.16.4", "raf": "~3.4.0", - "ts-jest": "^23.10.4", - "tslint": "^5.11.0", - "tslint-config-prettier": "^1.15.0", - "tslint-plugin-prettier": "^2.0.0", + "ts-jest": "^23.10.5", + "tslint": "^5.12.1", + "tslint-config-prettier": "^1.18.0", + "tslint-plugin-prettier": "^2.0.1", "tslint-react": "^3.6.0", "wait-on": "^2.1.0" }, @@ -63,6 +63,6 @@ "cypress" ], "dependencies": { - "typescript": "^3.1.3" + "typescript": "^3.3.3" } } diff --git a/packages/ra-core/src/actions/accumulateActions.ts b/packages/ra-core/src/actions/accumulateActions.ts index 8181e3d9c44..896f0358597 100644 --- a/packages/ra-core/src/actions/accumulateActions.ts +++ b/packages/ra-core/src/actions/accumulateActions.ts @@ -14,9 +14,6 @@ export interface CrudGetManyAccumulateAction { }; } -// Used to type the dispatcher function (the one injected by mapDispatchToProps) -export type CrudGetManyAccumulate = (resource: string, ids: any[]) => void; - export const crudGetManyAccumulate = ( resource: string, ids: any[] @@ -37,14 +34,6 @@ export interface CrudGetMatchingAccumulateAction { }; } -export type CrudGetMatchingAccumulate = ( - reference: string, - relatedTo: string, - pagination: Pagination, - sort: Sort, - filter: object -) => void; - export const crudGetMatchingAccumulate = ( reference: string, relatedTo: string, diff --git a/packages/ra-core/src/actions/dataActions/crudCreate.ts b/packages/ra-core/src/actions/dataActions/crudCreate.ts index e342ac848e2..93b853d2177 100644 --- a/packages/ra-core/src/actions/dataActions/crudCreate.ts +++ b/packages/ra-core/src/actions/dataActions/crudCreate.ts @@ -6,13 +6,6 @@ import { RedirectionSideEffect, } from '../../sideEffect'; -export type CrudCreate = ( - resource: string, - data: any, - basePath: string, - redirectTo: RedirectionSideEffect -) => void; - export const crudCreate = ( resource: string, data: any, diff --git a/packages/ra-core/src/actions/dataActions/crudGetList.ts b/packages/ra-core/src/actions/dataActions/crudGetList.ts index e9ef3009fba..b6a256f48de 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetList.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetList.ts @@ -3,13 +3,6 @@ import { GET_LIST } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; -export type CrudGetList = ( - resource: string, - pagination: Pagination, - sort: Sort, - filter: object -) => void; - export const crudGetList = ( resource: string, pagination: Pagination, diff --git a/packages/ra-core/src/actions/dataActions/crudGetMany.ts b/packages/ra-core/src/actions/dataActions/crudGetMany.ts index ee4edfe5ce3..edf3fe78f35 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetMany.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetMany.ts @@ -3,8 +3,6 @@ import { GET_MANY } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; -export type CrudGetMany = (resource: string, ids: Identifier[]) => void; - export const crudGetMany = ( resource: string, ids: Identifier[] diff --git a/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts b/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts index 53b8dd62bdf..a0dc625af53 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetManyReference.ts @@ -3,17 +3,6 @@ import { GET_MANY_REFERENCE } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; -export type CrudGetManyReference = ( - reference: string, - target: string, - id: Identifier, - relatedTo: string, - pagination: Pagination, - sort: Sort, - filter: object, - source: string -) => void; - export const crudGetManyReference = ( reference: string, target: string, diff --git a/packages/ra-core/src/actions/dataActions/crudGetMatching.ts b/packages/ra-core/src/actions/dataActions/crudGetMatching.ts index 02eb7812f93..ad3bfce9f78 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetMatching.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetMatching.ts @@ -3,14 +3,6 @@ import { GET_LIST } from '../../dataFetchActions'; import { FETCH_END, FETCH_ERROR } from '../fetchActions'; import { NotificationSideEffect } from '../../sideEffect'; -export type CrudGetMatching = ( - reference: string, - relatedTo: string, - pagination: Pagination, - sort: Sort, - filter: object -) => void; - export const crudGetMatching = ( reference: string, relatedTo: string, diff --git a/packages/ra-core/src/actions/dataActions/crudGetOne.ts b/packages/ra-core/src/actions/dataActions/crudGetOne.ts index 54d4a539e3c..295ab3334a3 100644 --- a/packages/ra-core/src/actions/dataActions/crudGetOne.ts +++ b/packages/ra-core/src/actions/dataActions/crudGetOne.ts @@ -7,13 +7,6 @@ import { RefreshSideEffect, } from '../../sideEffect'; -export type CrudGetOne = ( - resource: string, - id: Identifier, - basePath: string, - refresh?: RefreshSideEffect -) => void; - export const crudGetOne = ( resource: string, id: Identifier, diff --git a/packages/ra-core/src/actions/dataActions/crudUpdate.ts b/packages/ra-core/src/actions/dataActions/crudUpdate.ts index 7b7fc99cd55..5f470039cd5 100644 --- a/packages/ra-core/src/actions/dataActions/crudUpdate.ts +++ b/packages/ra-core/src/actions/dataActions/crudUpdate.ts @@ -6,15 +6,6 @@ import { RedirectionSideEffect, } from '../../sideEffect'; -export type CrudUpdate = ( - resource: string, - id: Identifier, - data: any, - previousData: any, - basePath: string, - redirectTo: RedirectionSideEffect -) => void; - export const crudUpdate = ( resource: string, id: Identifier, diff --git a/packages/ra-core/src/actions/listActions.ts b/packages/ra-core/src/actions/listActions.ts index daf0139e694..77158554d4b 100644 --- a/packages/ra-core/src/actions/listActions.ts +++ b/packages/ra-core/src/actions/listActions.ts @@ -14,11 +14,6 @@ export interface ChangeListParamsAction { readonly meta: { resource: string }; } -export type ChangeListParams = ( - resource: string, - params: Partial -) => void; - export const changeListParams = ( resource: string, params: ListParams diff --git a/packages/ra-core/src/actions/undoActions.ts b/packages/ra-core/src/actions/undoActions.ts index 12a150975ba..fd6da504c2e 100644 --- a/packages/ra-core/src/actions/undoActions.ts +++ b/packages/ra-core/src/actions/undoActions.ts @@ -5,8 +5,6 @@ export interface StartUndoableAction { readonly payload: any; } -export type StartUndoable = (action: any) => void; - export const startUndoable = (action: any): StartUndoableAction => ({ type: UNDOABLE, payload: { action }, diff --git a/packages/ra-core/src/controller/CreateController.tsx b/packages/ra-core/src/controller/CreateController.tsx index 98a8f8494f0..47f6dd8f9cd 100644 --- a/packages/ra-core/src/controller/CreateController.tsx +++ b/packages/ra-core/src/controller/CreateController.tsx @@ -5,11 +5,11 @@ import inflection from 'inflection'; import { parse } from 'query-string'; import withTranslate from '../i18n/translate'; -import { crudCreate as crudCreateAction, CrudCreate } from '../actions'; +import { crudCreate as crudCreateAction } from '../actions'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; import { Location } from 'history'; import { match as Match } from 'react-router'; -import { Record, Translate } from '../types'; +import { Record, Translate, Dispatch } from '../types'; import { RedirectionSideEffect } from '../sideEffect'; interface ChildrenFuncParams { @@ -26,7 +26,7 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudCreate: CrudCreate; + crudCreate: Dispatch; hasCreate: boolean; hasEdit: boolean; hasList: boolean; diff --git a/packages/ra-core/src/controller/EditController.tsx b/packages/ra-core/src/controller/EditController.tsx index eddc37f5ad3..0dc96602014 100644 --- a/packages/ra-core/src/controller/EditController.tsx +++ b/packages/ra-core/src/controller/EditController.tsx @@ -8,13 +8,10 @@ import { crudGetOne, crudUpdate, startUndoable as startUndoableAction, - CrudGetOne, - CrudUpdate, - StartUndoable, } from '../actions'; import { REDUX_FORM_NAME } from '../form'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; -import { Translate, Record } from '../types'; +import { Translate, Record, Dispatch } from '../types'; import { RedirectionSideEffect } from '../sideEffect'; interface ChildrenFuncParams { @@ -32,8 +29,8 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetOne: CrudGetOne; - dispatchCrudUpdate: CrudUpdate; + crudGetOne: Dispatch; + dispatchCrudUpdate: Dispatch; record?: Record; hasCreate: boolean; hasEdit: boolean; @@ -45,7 +42,7 @@ interface Props { match: object; resetForm: (form: string) => void; resource: string; - startUndoable: StartUndoable; + startUndoable: Dispatch; title: string | ReactNode; translate: Translate; undoable?: boolean; diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index 872280d3374..59ec9fb8b68 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -18,21 +18,24 @@ import queryReducer, { SET_FILTER, SORT_DESC, } from '../reducer/admin/resource/list/queryReducer'; -import { - crudGetList as crudGetListAction, - CrudGetList, -} from '../actions/dataActions'; +import { crudGetList as crudGetListAction } from '../actions/dataActions'; import { changeListParams as changeListParamsAction, setListSelectedIds as setListSelectedIdsAction, toggleListItem as toggleListItemAction, ListParams, - ChangeListParams, } from '../actions/listActions'; import withTranslate from '../i18n/translate'; import removeKey from '../util/removeKey'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; -import { Sort, AuthProvider, RecordMap, Identifier, Translate } from '../types'; +import { + Sort, + AuthProvider, + RecordMap, + Identifier, + Translate, + Dispatch, +} from '../types'; import { Location, LocationDescriptorObject, LocationState } from 'history'; interface ChildrenFuncParams { @@ -76,8 +79,8 @@ interface Props { // the props managed by react-admin authProvider: AuthProvider; basePath: string; - changeListParams: ChangeListParams; - crudGetList: CrudGetList; + changeListParams: Dispatch; + crudGetList: Dispatch; data?: RecordMap; debounce: number; hasCreate: boolean; diff --git a/packages/ra-core/src/controller/ShowController.tsx b/packages/ra-core/src/controller/ShowController.tsx index 272bef151fb..c083d1b3f65 100644 --- a/packages/ra-core/src/controller/ShowController.tsx +++ b/packages/ra-core/src/controller/ShowController.tsx @@ -3,9 +3,9 @@ import { connect } from 'react-redux'; import compose from 'recompose/compose'; import inflection from 'inflection'; import withTranslate from '../i18n/translate'; -import { crudGetOne as crudGetOneAction, CrudGetOne } from '../actions'; +import { crudGetOne as crudGetOneAction } from '../actions'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; -import { Translate, Record } from '../types'; +import { Translate, Record, Dispatch } from '../types'; interface ChildrenFuncParams { isLoading: boolean; @@ -21,7 +21,7 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetOne: CrudGetOne; + crudGetOne: Dispatch; record?: Record; hasCreate: boolean; hasEdit: boolean; diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx index 811b0560b3b..2aa0bd96bbf 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx @@ -2,12 +2,9 @@ import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import get from 'lodash/get'; -import { - crudGetManyAccumulate as crudGetManyAccumulateAction, - CrudGetManyAccumulate, -} from '../../actions'; +import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions'; import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; -import { ReduxState, Record, RecordMap } from '../../types'; +import { ReduxState, Record, RecordMap, Dispatch } from '../../types'; interface ChildrenFuncParams { loadedOnce: boolean; @@ -20,7 +17,7 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetManyAccumulate: CrudGetManyAccumulate; + crudGetManyAccumulate: Dispatch; data?: RecordMap; ids: any[]; record?: Record; diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx index 03dbfccab78..642b6f42ad9 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx @@ -2,12 +2,9 @@ import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import get from 'lodash/get'; -import { - crudGetManyAccumulate as crudGetManyAccumulateAction, - CrudGetManyAccumulate, -} from '../../actions'; +import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions'; import { linkToRecord } from '../../util'; -import { Record } from '../../types'; +import { Record, Dispatch } from '../../types'; interface ChildrenFuncParams { isLoading: boolean; @@ -19,7 +16,7 @@ interface Props { allowEmpty?: boolean; basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetManyAccumulate: CrudGetManyAccumulate; + crudGetManyAccumulate: Dispatch; record?: Record; reference: string; referenceRecord?: Record; diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index 1ff8344547f..c6869361ce3 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -2,10 +2,7 @@ import { Component, ReactNode } from 'react'; import { connect } from 'react-redux'; import isEqual from 'lodash/isEqual'; -import { - crudGetManyReference as crudGetManyReferenceAction, - CrudGetManyReference, -} from '../../actions'; +import { crudGetManyReference as crudGetManyReferenceAction } from '../../actions'; import { SORT_ASC, SORT_DESC, @@ -16,7 +13,7 @@ import { getTotal, nameRelatedTo, } from '../../reducer/admin/references/oneToMany'; -import { Record, Sort, RecordMap, Identifier } from '../../types'; +import { Record, Sort, RecordMap, Identifier, Dispatch } from '../../types'; interface ChildrenFuncParams { currentSort: Sort; @@ -35,7 +32,7 @@ interface ChildrenFuncParams { interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetManyReference: CrudGetManyReference; + crudGetManyReference: Dispatch; data?: RecordMap; filter?: any; ids?: any[]; diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx index 69680f9328a..dad400acd8b 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx @@ -8,8 +8,6 @@ import isEqual from 'lodash/isEqual'; import { crudGetMany as crudGetManyAction, crudGetMatching as crudGetMatchingAction, - CrudGetMatching, - CrudGetMany, } from '../../actions/dataActions'; import { getPossibleReferences, @@ -18,7 +16,7 @@ import { } from '../../reducer'; import { getStatusForArrayInput as getDataStatus } from './referenceDataStatus'; import withTranslate from '../../i18n/translate'; -import { Record, Sort, Translate, Pagination } from '../../types'; +import { Record, Sort, Translate, Pagination, Dispatch } from '../../types'; import { MatchingReferencesError } from './types'; const defaultReferenceSource = (resource: string, source: string) => @@ -39,8 +37,8 @@ interface Props { allowEmpty: boolean; basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetMatching: CrudGetMatching; - crudGetMany: CrudGetMany; + crudGetMatching: Dispatch; + crudGetMany: Dispatch; filter?: object; filterToQuery: (filter: {}) => any; input?: { diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.tsx index 6e5630c5098..d0ad99b2f20 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.tsx @@ -8,8 +8,6 @@ import isEqual from 'lodash/isEqual'; import { crudGetManyAccumulate as crudGetManyAccumulateAction, crudGetMatchingAccumulate as crudGetMatchingAccumulateAction, - CrudGetMatchingAccumulate, - CrudGetManyAccumulate, } from '../../actions/accumulateActions'; import { getPossibleReferences, @@ -18,7 +16,7 @@ import { } from '../../reducer'; import { getStatusForInput as getDataStatus } from './referenceDataStatus'; import withTranslate from '../../i18n/translate'; -import { Sort, Translate, Record, Pagination } from '../../types'; +import { Sort, Translate, Record, Pagination, Dispatch } from '../../types'; import { MatchingReferencesError } from './types'; const defaultReferenceSource = (resource: string, source: string) => @@ -42,8 +40,8 @@ interface Props { allowEmpty?: boolean; basePath: string; children: (params: ChildrenFuncParams) => ReactNode; - crudGetMatchingAccumulate: CrudGetMatchingAccumulate; - crudGetManyAccumulate: CrudGetManyAccumulate; + crudGetMatchingAccumulate: Dispatch; + crudGetManyAccumulate: Dispatch; filter?: object; filterToQuery: (filter: {}) => any; input?: { diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index f129ca69401..a2673779201 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -56,3 +56,7 @@ export interface ReduxState { messages: object; }; } + +export type Dispatch = T extends (...args: infer A) => any + ? (...args: A) => void + : never; diff --git a/yarn.lock b/yarn.lock index 8040a490280..fb6314e19a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9217,6 +9217,11 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" +lines-and-columns@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00" + integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= + listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" @@ -10903,6 +10908,11 @@ path-parse@^1.0.5: resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1" integrity sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME= +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -12935,6 +12945,13 @@ resolve@1.8.1, resolve@^1.1.6, resolve@^1.3.2, resolve@^1.5.0, resolve@^1.6.0, r dependencies: path-parse "^1.0.5" +resolve@1.x: + version "1.10.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.10.0.tgz#3bdaaeaf45cc07f375656dfd2e54ed0810b101ba" + integrity sha512-3sUr9aq5OfSg2S9pNtPA9hL1FVEAjvfOC4leW0SNf/mpnaakz2a9femSd6LqAww2RaFctwyf1lCqnTHuF1rxDg== + dependencies: + path-parse "^1.0.6" + responselike@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/responselike/-/responselike-1.0.2.tgz#918720ef3b631c5642be068f15ade5a46f4ba1e7" @@ -14342,10 +14359,10 @@ tryer@^1.0.0: resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8" integrity sha512-c3zayb8/kWWpycWYg87P71E1S1ZL6b6IJxfb5fvsUgsf0S2MVGaDhDXXjDMpdCpfWXqptc+4mXwmiy1ypXqRAA== -ts-jest@^23.10.4: - version "23.10.4" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.10.4.tgz#a7a953f55c9165bcaa90ff91014a178e87fe0df8" - integrity sha512-oV/wBwGUS7olSk/9yWMiSIJWbz5xO4zhftnY3gwv6s4SMg6WHF1m8XZNBvQOKQRiTAexZ9754Z13dxBq3Zgssw== +ts-jest@^23.10.5: + version "23.10.5" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-23.10.5.tgz#cdb550df4466a30489bf70ba867615799f388dd5" + integrity sha512-MRCs9qnGoyKgFc8adDEntAOP64fWK1vZKnOYU1o2HxaqjdJvGqmkLCPCnVq1/If4zkUmEjKPnCiUisTrlX2p2A== dependencies: bs-logger "0.x" buffer-from "1.x" @@ -14353,6 +14370,7 @@ ts-jest@^23.10.4: json5 "2.x" make-error "1.x" mkdirp "0.x" + resolve "1.x" semver "^5.5" yargs-parser "10.x" @@ -14361,17 +14379,18 @@ tslib@^1.7.1, tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" integrity sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ== -tslint-config-prettier@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.15.0.tgz#76b9714399004ab6831fdcf76d89b73691c812cf" - integrity sha512-06CgrHJxJmNYVgsmeMoa1KXzQRoOdvfkqnJth6XUkNeOz707qxN0WfxfhYwhL5kXHHbYJRby2bqAPKwThlZPhw== +tslint-config-prettier@^1.18.0: + version "1.18.0" + resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz#75f140bde947d35d8f0d238e0ebf809d64592c37" + integrity sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg== -tslint-plugin-prettier@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/tslint-plugin-prettier/-/tslint-plugin-prettier-2.0.0.tgz#ade328b26c71f37418d4d01187dca232a7447b49" - integrity sha512-nA8yM+1tS9dylirSajTxxFV6jCQrIMQ0Ykl//jjRgqmwwmGp3hqodG+rtr16S/OUwyQBfoFScFDK7nuHYPd4Gw== +tslint-plugin-prettier@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/tslint-plugin-prettier/-/tslint-plugin-prettier-2.0.1.tgz#95b6a3b766622ffc44375825d7760225c50c3680" + integrity sha512-4FX9JIx/1rKHIPJNfMb+ooX1gPk5Vg3vNi7+dyFYpLO+O57F4g+b/fo1+W/G0SUOkBLHB/YKScxjX/P+7ZT/Tw== dependencies: eslint-plugin-prettier "^2.2.0" + lines-and-columns "^1.1.6" tslib "^1.7.1" tslint-react@^3.6.0: @@ -14381,10 +14400,10 @@ tslint-react@^3.6.0: dependencies: tsutils "^2.13.1" -tslint@^5.11.0: - version "5.11.0" - resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.11.0.tgz#98f30c02eae3cde7006201e4c33cb08b48581eed" - integrity sha1-mPMMAurjzecAYgHkwzywi0hYHu0= +tslint@^5.12.1: + version "5.12.1" + resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.12.1.tgz#8cec9d454cf8a1de9b0a26d7bdbad6de362e52c1" + integrity sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw== dependencies: babel-code-frame "^6.22.0" builtin-modules "^1.1.1" @@ -14443,10 +14462,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.1.3.tgz#01b70247a6d3c2467f70c45795ef5ea18ce191d5" - integrity sha512-+81MUSyX+BaSo+u2RbozuQk/UWx6hfG0a5gHu4ANEM4sU96XbuIyAB+rWBW1u70c6a5QuZfuYICn3s2UjuHUpA== +typescript@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.3.3.tgz#f1657fc7daa27e1a8930758ace9ae8da31403221" + integrity sha512-Y21Xqe54TBVp+VDSNbuDYdGw0BpoR/Q6wo/+35M8PAU0vipahnyduJWirxxdxjsAkS7hue53x2zp8gz7F05u0A== ua-parser-js@^0.7.18: version "0.7.18" From 1be9fb53674afa073b8069be3a59cb22c19710a0 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Thu, 14 Feb 2019 11:07:51 +0100 Subject: [PATCH 18/35] Fix list typings --- packages/ra-core/src/controller/ListController.tsx | 4 ++-- packages/ra-core/src/reducer/admin/resource/list/params.ts | 2 +- .../ra-core/src/reducer/admin/resource/list/queryReducer.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index 59ec9fb8b68..7227d3273a9 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -95,7 +95,7 @@ interface Props { path: string; params: ListParams; push: (location: LocationDescriptorObject) => void; - query: Partial; + query: ListParams; resource: string; setSelectedIds: (resource: string, ids: Identifier[]) => void; toggleItem: (resource: string, id: Identifier) => void; @@ -282,7 +282,7 @@ export class ListControllerView extends Component { if (!query.page) { query.page = 1; } - return query; + return query as ListParams; } getFilterValues() { diff --git a/packages/ra-core/src/reducer/admin/resource/list/params.ts b/packages/ra-core/src/reducer/admin/resource/list/params.ts index ec93577601f..237507a0bf8 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/params.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/params.ts @@ -30,7 +30,7 @@ const paramsReducer: Reducer = ( ) => { switch (action.type) { case CRUD_CHANGE_LIST_PARAMS: - return action.payload; + return { ...previousState, ...action.payload }; default: return previousState; } diff --git a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts index d860f6f25ae..14bec195df2 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts @@ -15,7 +15,7 @@ const oppositeOrder = direction => /** * This reducer is for the react-router query string, NOT for redux. */ -const queryReducer: Reducer> = ( +const queryReducer: Reducer = ( previousState, { type, payload } ) => { From a2201e1a239f2b158f1c7d5014f5a971bfbdd13e Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Thu, 14 Feb 2019 11:15:13 +0100 Subject: [PATCH 19/35] Accumulate typings --- packages/ra-core/src/actions/accumulateActions.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ra-core/src/actions/accumulateActions.ts b/packages/ra-core/src/actions/accumulateActions.ts index 896f0358597..a0fba225e41 100644 --- a/packages/ra-core/src/actions/accumulateActions.ts +++ b/packages/ra-core/src/actions/accumulateActions.ts @@ -1,5 +1,5 @@ import { crudGetMany, crudGetMatching } from './dataActions'; -import { Pagination, Sort } from '../types'; +import { Pagination, Sort, Identifier } from '../types'; export const CRUD_GET_MANY_ACCUMULATE = 'RA/CRUD_GET_MANY_ACCUMULATE'; @@ -7,7 +7,7 @@ export interface CrudGetManyAccumulateAction { readonly type: typeof CRUD_GET_MANY_ACCUMULATE; readonly payload: { resource: string; - ids: any[]; + ids: Identifier[]; }; readonly meta: { accumulate: any; @@ -16,7 +16,7 @@ export interface CrudGetManyAccumulateAction { export const crudGetManyAccumulate = ( resource: string, - ids: any[] + ids: Identifier[] ): CrudGetManyAccumulateAction => ({ type: CRUD_GET_MANY_ACCUMULATE, payload: { resource, ids }, From 3b67ba8a36a0fb6f4f7a5fb9d1f9bf4ac7992835 Mon Sep 17 00:00:00 2001 From: kujon Date: Mon, 18 Feb 2019 13:54:18 +0000 Subject: [PATCH 20/35] Corrected prop types for ArrayInput --- packages/ra-ui-materialui/src/input/ArrayInput.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/ra-ui-materialui/src/input/ArrayInput.js b/packages/ra-ui-materialui/src/input/ArrayInput.js index 7cea3e0f7fc..536ef0d1e42 100644 --- a/packages/ra-ui-materialui/src/input/ArrayInput.js +++ b/packages/ra-ui-materialui/src/input/ArrayInput.js @@ -107,7 +107,10 @@ ArrayInput.propTypes = { source: PropTypes.string, record: PropTypes.object, options: PropTypes.object, - validate: PropTypes.func, + validate: PropTypes.oneOfType([ + PropTypes.func, + PropTypes.arrayOf(PropTypes.func), + ]), }; ArrayInput.defaultProps = { From 274acf94984dc6eb0d28d28f27c4ebd1760e0311 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 20 Feb 2019 07:37:43 +0100 Subject: [PATCH 21/35] Review --- .../src/controller/CreateController.spec.tsx | 2 +- .../src/controller/CreateController.tsx | 11 ++++----- .../ra-core/src/controller/EditController.tsx | 13 +++++----- .../src/controller/ListController.spec.tsx | 2 +- .../ra-core/src/controller/ListController.tsx | 9 ++++--- .../ra-core/src/controller/ShowController.tsx | 22 ++++++++--------- .../ReferenceArrayFieldController.spec.tsx | 2 +- .../field/ReferenceArrayFieldController.tsx | 24 +++++++++++++------ .../field/ReferenceFieldController.spec.tsx | 2 +- .../field/ReferenceFieldController.tsx | 4 ++-- .../ReferenceManyFieldController.spec.tsx | 2 +- .../field/ReferenceManyFieldController.tsx | 22 ++++++++++++----- .../ReferenceArrayInputController.spec.tsx | 2 +- .../input/ReferenceArrayInputController.tsx | 4 ++-- .../input/ReferenceInputController.spec.tsx | 2 +- .../input/ReferenceInputController.tsx | 7 ++++-- .../src/reducer/admin/references/oneToMany.ts | 2 +- .../src/reducer/admin/resource/list/params.ts | 2 +- 18 files changed, 80 insertions(+), 54 deletions(-) diff --git a/packages/ra-core/src/controller/CreateController.spec.tsx b/packages/ra-core/src/controller/CreateController.spec.tsx index b37e6823d82..111d86db683 100644 --- a/packages/ra-core/src/controller/CreateController.spec.tsx +++ b/packages/ra-core/src/controller/CreateController.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { CreateControllerView as CreateController } from './CreateController'; +import { UnconnectedCreateController as CreateController } from './CreateController'; describe('CreateController', () => { describe('Presetting the record from the location', () => { diff --git a/packages/ra-core/src/controller/CreateController.tsx b/packages/ra-core/src/controller/CreateController.tsx index 47f6dd8f9cd..eb9f3888424 100644 --- a/packages/ra-core/src/controller/CreateController.tsx +++ b/packages/ra-core/src/controller/CreateController.tsx @@ -34,9 +34,8 @@ interface Props { isLoading: boolean; location: Location; match: Match; - record?: Record; + record?: Partial; resource: string; - title: string | ReactNode; translate: Translate; } @@ -81,12 +80,12 @@ interface Props { * ); * export default App; */ -export class CreateControllerView extends Component { - public static defaultProps = { +export class UnconnectedCreateController extends Component { + public static defaultProps: Partial = { record: {}, }; - private record; + private record: Partial; constructor(props) { super(props); @@ -168,6 +167,6 @@ const CreateController = compose( { crudCreate: crudCreateAction } ), withTranslate -)(CreateControllerView); +)(UnconnectedCreateController); export default CreateController; diff --git a/packages/ra-core/src/controller/EditController.tsx b/packages/ra-core/src/controller/EditController.tsx index 0dc96602014..6fc7367ca2f 100644 --- a/packages/ra-core/src/controller/EditController.tsx +++ b/packages/ra-core/src/controller/EditController.tsx @@ -11,7 +11,7 @@ import { } from '../actions'; import { REDUX_FORM_NAME } from '../form'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; -import { Translate, Record, Dispatch } from '../types'; +import { Translate, Record, Dispatch, Identifier } from '../types'; import { RedirectionSideEffect } from '../sideEffect'; interface ChildrenFuncParams { @@ -36,14 +36,13 @@ interface Props { hasEdit: boolean; hasShow: boolean; hasList: boolean; - id: string; + id: Identifier; isLoading: boolean; location: object; match: object; resetForm: (form: string) => void; resource: string; startUndoable: Dispatch; - title: string | ReactNode; translate: Translate; undoable?: boolean; version: number; @@ -91,7 +90,7 @@ interface Props { * ); * export default App; */ -export class EditController extends Component { +export class UnconnectedEditController extends Component { componentDidMount() { this.updateData(); } @@ -194,7 +193,7 @@ function mapStateToProps(state, props) { }; } -export default compose( +const EditController = compose( checkMinimumRequiredProps('Edit', ['basePath', 'resource']), connect( mapStateToProps, @@ -206,4 +205,6 @@ export default compose( } ), withTranslate -)(EditController); +)(UnconnectedEditController); + +export default EditController; diff --git a/packages/ra-core/src/controller/ListController.spec.tsx b/packages/ra-core/src/controller/ListController.spec.tsx index 8276c7e94f9..3a3534c69b6 100644 --- a/packages/ra-core/src/controller/ListController.spec.tsx +++ b/packages/ra-core/src/controller/ListController.spec.tsx @@ -3,7 +3,7 @@ import { shallow } from 'enzyme'; import lolex from 'lolex'; import { - ListControllerView as ListController, + UnconnectedListController as ListController, getListControllerProps, sanitizeListRestProps, } from './ListController'; diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index 7227d3273a9..c746e65bf6f 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -145,7 +145,7 @@ interface Props { * * ); */ -export class ListControllerView extends Component { +export class UnconnectedListController extends Component { public static defaultProps: Partial = { debounce: 500, filter: {}, @@ -399,7 +399,10 @@ export class ListControllerView extends Component { (typeof query.page === 'string' ? parseInt(query.page, 10) : query.page) || 1, - perPage: parseInt(query.perPage.toString(), 10), + perPage: + (typeof query.perPage === 'string' + ? parseInt(query.perPage, 10) + : query.perPage) || 10, resource, selectedIds, setFilters: this.setFilters, @@ -513,6 +516,6 @@ const ListController = compose( } ), withTranslate -)(ListControllerView); +)(UnconnectedListController); export default ListController; diff --git a/packages/ra-core/src/controller/ShowController.tsx b/packages/ra-core/src/controller/ShowController.tsx index c083d1b3f65..ef6fbd777ee 100644 --- a/packages/ra-core/src/controller/ShowController.tsx +++ b/packages/ra-core/src/controller/ShowController.tsx @@ -5,7 +5,9 @@ import inflection from 'inflection'; import withTranslate from '../i18n/translate'; import { crudGetOne as crudGetOneAction } from '../actions'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; -import { Translate, Record, Dispatch } from '../types'; +import { Translate, Record, Dispatch, Identifier } from '../types'; +import { Location } from 'history'; +import { match as Match } from 'react-router'; interface ChildrenFuncParams { isLoading: boolean; @@ -13,7 +15,6 @@ interface ChildrenFuncParams { resource: string; basePath: string; record?: Record; - title: string | ReactNode; translate: Translate; version: number; } @@ -27,12 +28,11 @@ interface Props { hasEdit: boolean; hasShow: boolean; hasList: boolean; - id: string; + id: Identifier; isLoading: boolean; - location: object; - match: object; + location: Location; + match: Match; resource: string; - title: string | ReactNode; translate: Translate; version: number; } @@ -79,7 +79,7 @@ interface Props { * ); * export default App; */ -export class ShowController extends Component { +export class UnconnectedShowController extends Component { componentDidMount() { this.updateData(); } @@ -105,7 +105,6 @@ export class ShowController extends Component { isLoading, record, resource, - title, translate, version, } = this.props; @@ -125,7 +124,6 @@ export class ShowController extends Component { }); return children({ isLoading, - title, defaultTitle, resource, basePath, @@ -147,11 +145,13 @@ function mapStateToProps(state, props) { }; } -export default compose( +const ShowController = compose( checkMinimumRequiredProps('Show', ['basePath', 'resource']), connect( mapStateToProps, { crudGetOne: crudGetOneAction } ), withTranslate -)(ShowController); +)(UnconnectedShowController); + +export default ShowController; diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx index 4dbbaf8e7f6..8f668082bae 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; -import { ReferenceArrayFieldControllerView as ReferenceArrayFieldController } from './ReferenceArrayFieldController'; +import { UnconnectedReferenceArrayFieldController as ReferenceArrayFieldController } from './ReferenceArrayFieldController'; describe('', () => { const crudGetManyAccumulate = jest.fn(); diff --git a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx index 2aa0bd96bbf..a591e8e24c9 100644 --- a/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceArrayFieldController.tsx @@ -4,14 +4,21 @@ import get from 'lodash/get'; import { crudGetManyAccumulate as crudGetManyAccumulateAction } from '../../actions'; import { getReferencesByIds } from '../../reducer/admin/references/oneToMany'; -import { ReduxState, Record, RecordMap, Dispatch } from '../../types'; +import { + ReduxState, + Record, + RecordMap, + Dispatch, + Sort, + Identifier, +} from '../../types'; interface ChildrenFuncParams { loadedOnce: boolean; - ids: any[]; + ids: Identifier[]; data: RecordMap; referenceBasePath: string; - currentSort: any; + currentSort: Sort; } interface Props { @@ -19,7 +26,7 @@ interface Props { children: (params: ChildrenFuncParams) => ReactNode; crudGetManyAccumulate: Dispatch; data?: RecordMap; - ids: any[]; + ids: Identifier[]; record?: Record; reference: string; resource: string; @@ -58,7 +65,7 @@ interface Props { * * */ -export class ReferenceArrayFieldControllerView extends Component { +export class UnconnectedReferenceArrayFieldController extends Component { componentDidMount() { this.fetchReferences(); } @@ -94,7 +101,10 @@ export class ReferenceArrayFieldControllerView extends Component { ids, data, referenceBasePath, - currentSort: {}, + currentSort: { + field: 'id', + order: 'ASC', + }, }); } } @@ -113,6 +123,6 @@ const ReferenceArrayFieldController = connect( { crudGetManyAccumulate: crudGetManyAccumulateAction, } -)(ReferenceArrayFieldControllerView); +)(UnconnectedReferenceArrayFieldController); export default ReferenceArrayFieldController; diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx index 13fd50b1efd..4a070970386 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldController.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; -import { ReferenceFieldControllerView as ReferenceFieldController } from './ReferenceFieldController'; +import { UnconnectedReferenceFieldController as ReferenceFieldController } from './ReferenceFieldController'; describe('', () => { it('should call crudGetManyAccumulate on componentDidMount if reference source is defined', () => { diff --git a/packages/ra-core/src/controller/field/ReferenceFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx index 642b6f42ad9..06bc814e5dc 100644 --- a/packages/ra-core/src/controller/field/ReferenceFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceFieldController.tsx @@ -54,7 +54,7 @@ interface Props { * * */ -export class ReferenceFieldControllerView extends Component { +export class UnconnectedReferenceFieldController extends Component { public static defaultProps: Partial = { allowEmpty: false, linkType: 'edit', @@ -117,6 +117,6 @@ const ReferenceFieldController = connect( { crudGetManyAccumulate: crudGetManyAccumulateAction, } -)(ReferenceFieldControllerView); +)(UnconnectedReferenceFieldController); export default ReferenceFieldController; diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx index 4c8e2141410..9e178a90685 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; import { render } from 'react-testing-library'; -import { ReferenceManyFieldControllerView as ReferenceManyFieldController } from './ReferenceManyFieldController'; +import { UnconnectedReferenceManyFieldController as ReferenceManyFieldController } from './ReferenceManyFieldController'; describe('', () => { it('should set loadedOnce to false when related records are not yet fetched', () => { diff --git a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx index c6869361ce3..cb75ac7dc10 100644 --- a/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx +++ b/packages/ra-core/src/controller/field/ReferenceManyFieldController.tsx @@ -47,6 +47,12 @@ interface Props { total?: number; } +interface State { + sort: Sort; + page: number; + perPage: number; +} + /** * Render related records to the current one. * @@ -93,7 +99,10 @@ interface Props { * ... * */ -export class ReferenceManyFieldControllerView extends Component { +export class UnconnectedReferenceManyFieldController extends Component< + Props, + State +> { public static defaultProps: Partial = { filter: {}, perPage: 25, @@ -101,7 +110,7 @@ export class ReferenceManyFieldControllerView extends Component { source: 'id', }; - public state = { + public state: State = { sort: this.props.sort, page: 1, perPage: this.props.perPage, @@ -111,7 +120,7 @@ export class ReferenceManyFieldControllerView extends Component { this.fetchReferences(); } - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: Props) { if ( this.props.record.id !== nextProps.record.id || !isEqual(this.props.filter, nextProps.filter) @@ -133,9 +142,10 @@ export class ReferenceManyFieldControllerView extends Component { this.setState({ sort: { field, order } }, this.fetchReferences); }; - setPage = page => this.setState({ page }, this.fetchReferences); + setPage = (page: number) => this.setState({ page }, this.fetchReferences); - setPerPage = perPage => this.setState({ perPage }, this.fetchReferences); + setPerPage = (perPage: number) => + this.setState({ perPage }, this.fetchReferences); fetchReferences( { reference, record, resource, target, filter, source } = this.props @@ -212,6 +222,6 @@ const ReferenceManyFieldController = connect( { crudGetManyReference: crudGetManyReferenceAction, } -)(ReferenceManyFieldControllerView); +)(UnconnectedReferenceManyFieldController); export default ReferenceManyFieldController; diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx index ba2cab9731e..10475086308 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.spec.tsx @@ -1,7 +1,7 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; -import { ReferenceArrayInputControllerView as ReferenceArrayInputController } from './ReferenceArrayInputController'; +import { UnconnectedReferenceArrayInputController as ReferenceArrayInputController } from './ReferenceArrayInputController'; describe('', () => { const defaultProps = { diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx index dad400acd8b..70504c61b01 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx @@ -136,7 +136,7 @@ interface Props { * * */ -export class ReferenceArrayInputControllerView extends Component { +export class UnconnectedReferenceArrayInputController extends Component { public static defaultProps = { allowEmpty: false, filter: {}, @@ -319,7 +319,7 @@ const ReferenceArrayInputController = compose( crudGetMatching: crudGetMatchingAction, } ) -)(ReferenceArrayInputControllerView); +)(UnconnectedReferenceArrayInputController); ReferenceArrayInputController.defaultProps = { referenceSource: defaultReferenceSource, // used in makeMapStateToProps diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx index 232a5a92475..c5617b602c9 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.spec.tsx @@ -2,7 +2,7 @@ import React from 'react'; import assert from 'assert'; import { shallow } from 'enzyme'; import { render } from 'react-testing-library'; -import { ReferenceInputControllerView as ReferenceInputController } from './ReferenceInputController'; +import { UnconnectedReferenceInputController as ReferenceInputController } from './ReferenceInputController'; describe('', () => { const defaultProps = { diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.tsx index d0ad99b2f20..19585e40d3c 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.tsx @@ -145,7 +145,10 @@ interface State { * * */ -export class ReferenceInputControllerView extends Component { +export class UnconnectedReferenceInputController extends Component< + Props, + State +> { public static defaultProps = { allowEmpty: false, filter: {}, @@ -312,7 +315,7 @@ const ReferenceInputController = compose( crudGetMatchingAccumulate: crudGetMatchingAccumulateAction, } ) -)(ReferenceInputControllerView); +)(UnconnectedReferenceInputController); ReferenceInputController.defaultProps = { referenceSource: defaultReferenceSource, // used in makeMapStateToProps diff --git a/packages/ra-core/src/reducer/admin/references/oneToMany.ts b/packages/ra-core/src/reducer/admin/references/oneToMany.ts index 964ff2dc06b..6341c3c0825 100644 --- a/packages/ra-core/src/reducer/admin/references/oneToMany.ts +++ b/packages/ra-core/src/reducer/admin/references/oneToMany.ts @@ -78,7 +78,7 @@ export const getReferences = (state: ReduxState, reference, relatedTo) => { export const getReferencesByIds = ( state: ReduxState, reference: string, - ids: any[] + ids: Identifier[] ) => { if (ids.length === 0) { return {}; diff --git a/packages/ra-core/src/reducer/admin/resource/list/params.ts b/packages/ra-core/src/reducer/admin/resource/list/params.ts index 237507a0bf8..ec93577601f 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/params.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/params.ts @@ -30,7 +30,7 @@ const paramsReducer: Reducer = ( ) => { switch (action.type) { case CRUD_CHANGE_LIST_PARAMS: - return { ...previousState, ...action.payload }; + return action.payload; default: return previousState; } From 4351e2beff9f4627cfe2eb8020e9e07c3cd47ef6 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 20 Feb 2019 07:52:55 +0100 Subject: [PATCH 22/35] Fix create controller --- packages/ra-core/src/controller/CreateController.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ra-core/src/controller/CreateController.tsx b/packages/ra-core/src/controller/CreateController.tsx index eb9f3888424..10cdee3a1c3 100644 --- a/packages/ra-core/src/controller/CreateController.tsx +++ b/packages/ra-core/src/controller/CreateController.tsx @@ -18,7 +18,7 @@ interface ChildrenFuncParams { save: (record: Partial, redirect: RedirectionSideEffect) => void; resource: string; basePath: string; - record?: Record; + record?: Partial; redirect: RedirectionSideEffect; translate: Translate; } From eb3f56db8eb9d2cf9b6fd352702d67d7ee0cbc36 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 20 Feb 2019 07:53:04 +0100 Subject: [PATCH 23/35] Added redux-form types --- packages/ra-core/package.json | 1 + .../input/ReferenceArrayInputController.tsx | 5 ++-- .../input/ReferenceInputController.tsx | 5 ++-- packages/ra-core/src/util/TestContext.tsx | 4 +-- yarn.lock | 25 ++++++++++++++++++- 5 files changed, 31 insertions(+), 9 deletions(-) diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 3e0f1a1c084..2a75f043672 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -30,6 +30,7 @@ "@types/node-polyglot": "^0.4.31", "@types/react-router": "^4.4.1", "@types/recompose": "^0.27.0", + "@types/redux-form": "^7.5.2", "cross-env": "^5.2.0", "enzyme": "~3.7.0", "enzyme-adapter-react-16": "~1.6.0", diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx index 70504c61b01..42607d4834e 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx @@ -4,6 +4,7 @@ import debounce from 'lodash/debounce'; import compose from 'recompose/compose'; import { createSelector } from 'reselect'; import isEqual from 'lodash/isEqual'; +import { WrappedFieldInputProps } from 'redux-form'; import { crudGetMany as crudGetManyAction, @@ -41,9 +42,7 @@ interface Props { crudGetMany: Dispatch; filter?: object; filterToQuery: (filter: {}) => any; - input?: { - value: any; - }; + input?: WrappedFieldInputProps; matchingReferences?: Record[] | MatchingReferencesError; meta?: object; onChange?: () => void; diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.tsx index 19585e40d3c..60af34c0350 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.tsx @@ -4,6 +4,7 @@ import debounce from 'lodash/debounce'; import compose from 'recompose/compose'; import { createSelector } from 'reselect'; import isEqual from 'lodash/isEqual'; +import { WrappedFieldInputProps } from 'redux-form'; import { crudGetManyAccumulate as crudGetManyAccumulateAction, @@ -44,9 +45,7 @@ interface Props { crudGetManyAccumulate: Dispatch; filter?: object; filterToQuery: (filter: {}) => any; - input?: { - value: any; - }; + input?: WrappedFieldInputProps; matchingReferences: Record[] | MatchingReferencesError; onChange: () => void; perPage: number; diff --git a/packages/ra-core/src/util/TestContext.tsx b/packages/ra-core/src/util/TestContext.tsx index b3b70933fbd..31b539a91a6 100644 --- a/packages/ra-core/src/util/TestContext.tsx +++ b/packages/ra-core/src/util/TestContext.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import { createStore, combineReducers } from 'redux'; +import { createStore } from 'redux'; import { Provider } from 'react-redux'; import { reducer as formReducer } from 'redux-form'; import TranslationProvider from '../i18n/TranslationProvider'; @@ -14,7 +14,7 @@ export const defaultStore = { references: { possibleValues: {} }, ui: { viewVersion: 1 }, }, - form: formReducer(), + form: formReducer({}, { type: '@@FOO' }), // Call the reducer with an unknown type to initialize it i18n: { locale: 'en', messages: {} }, }; diff --git a/yarn.lock b/yarn.lock index fb6314e19a9..de758336d54 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1152,6 +1152,14 @@ dependencies: "@types/react" "*" +"@types/redux-form@^7.5.2": + version "7.5.2" + resolved "https://registry.yarnpkg.com/@types/redux-form/-/redux-form-7.5.2.tgz#7f304cbfa03475a40a150eea85d88172c282916f" + integrity sha512-J/qacLqTclxrfhuS35p8fDUsWCW1/+QGsExTVGHDG3OTHelLSVeZx+bvTVnvT9XQDjASwLKPNezOqvQ0gEamDA== + dependencies: + "@types/react" "*" + redux "^3.6.0 || ^4.0.0" + "@types/sinon-chai@2.7.29": version "2.7.29" resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-2.7.29.tgz#4db01497e2dd1908b2bd30d1782f456353f5f723" @@ -8707,7 +8715,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" integrity sha1-mGbfOVECEw449/mWvOtlRDIJwls= -js-tokens@^4.0.0: +"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== @@ -9569,6 +9577,13 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0" +loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + loud-rejection@^1.0.0: version "1.6.0" resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" @@ -12593,6 +12608,14 @@ redux@^3.4.0, redux@~3.7.2: loose-envify "^1.1.0" symbol-observable "^1.0.3" +"redux@^3.6.0 || ^4.0.0": + version "4.0.1" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5" + integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg== + dependencies: + loose-envify "^1.4.0" + symbol-observable "^1.2.0" + redux@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03" From 458ae99a5597bb648a32eb343633db57b70c62c3 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 20 Feb 2019 08:14:47 +0100 Subject: [PATCH 24/35] Accept strings and numbers as identifiers for record maps --- packages/ra-core/src/types.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ra-core/src/types.ts b/packages/ra-core/src/types.ts index a2673779201..c2a065727de 100644 --- a/packages/ra-core/src/types.ts +++ b/packages/ra-core/src/types.ts @@ -5,7 +5,9 @@ export interface Record { } export interface RecordMap { + // Accept strings and numbers as identifiers [id: string]: Record; + [id: number]: Record; } export interface Sort { From 44970e782f73b6335fff00a6fe1d1515769aaa13 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 20 Feb 2019 14:50:13 +0100 Subject: [PATCH 25/35] Review Props --- .../src/controller/CreateController.tsx | 8 +++--- .../ra-core/src/controller/EditController.tsx | 10 +++---- .../ra-core/src/controller/ListController.tsx | 28 +++++++++---------- .../input/ReferenceArrayInputController.tsx | 2 +- .../input/ReferenceInputController.tsx | 2 +- 5 files changed, 24 insertions(+), 26 deletions(-) diff --git a/packages/ra-core/src/controller/CreateController.tsx b/packages/ra-core/src/controller/CreateController.tsx index 10cdee3a1c3..e686847ec1e 100644 --- a/packages/ra-core/src/controller/CreateController.tsx +++ b/packages/ra-core/src/controller/CreateController.tsx @@ -27,10 +27,10 @@ interface Props { basePath: string; children: (params: ChildrenFuncParams) => ReactNode; crudCreate: Dispatch; - hasCreate: boolean; - hasEdit: boolean; - hasList: boolean; - hasShow: boolean; + hasCreate?: boolean; + hasEdit?: boolean; + hasList?: boolean; + hasShow?: boolean; isLoading: boolean; location: Location; match: Match; diff --git a/packages/ra-core/src/controller/EditController.tsx b/packages/ra-core/src/controller/EditController.tsx index 6fc7367ca2f..66f22d09298 100644 --- a/packages/ra-core/src/controller/EditController.tsx +++ b/packages/ra-core/src/controller/EditController.tsx @@ -32,14 +32,12 @@ interface Props { crudGetOne: Dispatch; dispatchCrudUpdate: Dispatch; record?: Record; - hasCreate: boolean; - hasEdit: boolean; - hasShow: boolean; - hasList: boolean; + hasCreate?: boolean; + hasEdit?: boolean; + hasShow?: boolean; + hasList?: boolean; id: Identifier; isLoading: boolean; - location: object; - match: object; resetForm: (form: string) => void; resource: string; startUndoable: Dispatch; diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index c746e65bf6f..3aa9f5ab490 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -71,28 +71,28 @@ interface Props { // the props you can change children: (params: ChildrenFuncParams) => ReactNode; filter?: object; - filters: ReactElement; - filterDefaultValues: object; - pagination: ReactElement; + filters?: ReactElement; + filterDefaultValues?: object; + pagination?: ReactElement; perPage: number; sort: Sort; // the props managed by react-admin - authProvider: AuthProvider; + authProvider?: AuthProvider; basePath: string; changeListParams: Dispatch; crudGetList: Dispatch; data?: RecordMap; - debounce: number; - hasCreate: boolean; - hasEdit: boolean; - hasList: boolean; - hasShow: boolean; - ids: Identifier[]; - loadedOnce: boolean; - selectedIds: Identifier[]; + debounce?: number; + hasCreate?: boolean; + hasEdit?: boolean; + hasList?: boolean; + hasShow?: boolean; + ids?: Identifier[]; + loadedOnce?: boolean; + selectedIds?: Identifier[]; isLoading: boolean; location: Location; - path: string; + path?: string; params: ListParams; push: (location: LocationDescriptorObject) => void; query: ListParams; @@ -101,7 +101,7 @@ interface Props { toggleItem: (resource: string, id: Identifier) => void; total: number; translate: Translate; - version: number; + version?: number; } /** diff --git a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx index 42607d4834e..32dbf6a17bb 100644 --- a/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceArrayInputController.tsx @@ -35,7 +35,7 @@ interface ChildrenFuncParams { } interface Props { - allowEmpty: boolean; + allowEmpty?: boolean; basePath: string; children: (params: ChildrenFuncParams) => ReactNode; crudGetMatching: Dispatch; diff --git a/packages/ra-core/src/controller/input/ReferenceInputController.tsx b/packages/ra-core/src/controller/input/ReferenceInputController.tsx index 60af34c0350..f4c19940e00 100644 --- a/packages/ra-core/src/controller/input/ReferenceInputController.tsx +++ b/packages/ra-core/src/controller/input/ReferenceInputController.tsx @@ -46,7 +46,7 @@ interface Props { filter?: object; filterToQuery: (filter: {}) => any; input?: WrappedFieldInputProps; - matchingReferences: Record[] | MatchingReferencesError; + matchingReferences?: Record[] | MatchingReferencesError; onChange: () => void; perPage: number; record?: Record; From e7cb8ec0e97699ecc89ea43b9d2ffe3a25364a28 Mon Sep 17 00:00:00 2001 From: mnlbox Date: Wed, 20 Feb 2019 18:04:38 +0330 Subject: [PATCH 26/35] add a note about CloneButton component to docs --- docs/CreateEdit.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/CreateEdit.md b/docs/CreateEdit.md index c0d1266bcec..1a74c4f33e6 100644 --- a/docs/CreateEdit.md +++ b/docs/CreateEdit.md @@ -197,6 +197,8 @@ const PostList = props => ( Alternately, you may need to prepopulate a record based on a *related* record. For instance, in a `PostList` component, you may want to display a button to create a comment related to the current post. Clicking on that button would lead to a `CommentCreate` page where the `post_id` is preset to the id of the Post. +**Note** `` is designed to be used in an edit view _Actions_ component, not inside a _Toolbar_. The Toolbar is basically for submitting the form, not for going to another resource. + By default, the `` view starts with an empty `record`. However, if the `location` object (injected by [react-router](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` instead of the empty object. That's how the `` works behind the hood. That means that if you want to create a link to a creation form, presetting *some* values, all you have to do is to set the location `state`. React-router provides the `` component for that: From e6c89bc43fb3ec9a0d9c0f2e6afb4ffda7d2828e Mon Sep 17 00:00:00 2001 From: Francois Zaninotto Date: Wed, 20 Feb 2019 15:49:37 +0100 Subject: [PATCH 27/35] code formatting --- docs/CreateEdit.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/CreateEdit.md b/docs/CreateEdit.md index 1a74c4f33e6..9f57600a09b 100644 --- a/docs/CreateEdit.md +++ b/docs/CreateEdit.md @@ -197,7 +197,7 @@ const PostList = props => ( Alternately, you may need to prepopulate a record based on a *related* record. For instance, in a `PostList` component, you may want to display a button to create a comment related to the current post. Clicking on that button would lead to a `CommentCreate` page where the `post_id` is preset to the id of the Post. -**Note** `` is designed to be used in an edit view _Actions_ component, not inside a _Toolbar_. The Toolbar is basically for submitting the form, not for going to another resource. +**Note** `` is designed to be used in an edit view `` component, not inside a ``. The `Toolbar` is basically for submitting the form, not for going to another resource. By default, the `` view starts with an empty `record`. However, if the `location` object (injected by [react-router](https://reacttraining.com/react-router/web/api/location)) contains a `record` in its `state`, the `` view uses that `record` instead of the empty object. That's how the `` works behind the hood. From 25719affe27936bace0564c86fd9220c5f29a407 Mon Sep 17 00:00:00 2001 From: Gildas Garcia Date: Wed, 20 Feb 2019 16:07:06 +0100 Subject: [PATCH 28/35] Review ShowController props --- packages/ra-core/src/controller/ShowController.tsx | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/ra-core/src/controller/ShowController.tsx b/packages/ra-core/src/controller/ShowController.tsx index ef6fbd777ee..7310cb84300 100644 --- a/packages/ra-core/src/controller/ShowController.tsx +++ b/packages/ra-core/src/controller/ShowController.tsx @@ -6,8 +6,6 @@ import withTranslate from '../i18n/translate'; import { crudGetOne as crudGetOneAction } from '../actions'; import checkMinimumRequiredProps from './checkMinimumRequiredProps'; import { Translate, Record, Dispatch, Identifier } from '../types'; -import { Location } from 'history'; -import { match as Match } from 'react-router'; interface ChildrenFuncParams { isLoading: boolean; @@ -24,14 +22,12 @@ interface Props { children: (params: ChildrenFuncParams) => ReactNode; crudGetOne: Dispatch; record?: Record; - hasCreate: boolean; - hasEdit: boolean; - hasShow: boolean; - hasList: boolean; + hasCreate?: boolean; + hasEdit?: boolean; + hasShow?: boolean; + hasList?: boolean; id: Identifier; isLoading: boolean; - location: Location; - match: Match; resource: string; translate: Translate; version: number; From b3f450d9f10b201d17f2a876180c80d54a953f41 Mon Sep 17 00:00:00 2001 From: 0x0 <21711933@student.uwa.edu.au> Date: Thu, 21 Feb 2019 12:19:16 +0800 Subject: [PATCH 29/35] Fix typo in tutorial.md --- docs/Tutorial.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Tutorial.md b/docs/Tutorial.md index b379dfeeab4..18bf7be54de 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -764,7 +764,7 @@ This works exactly the way you expect. The lesson here is that react-admin takes ## Connecting To A Real API -Here is the elephant in the room of this tutorial. In real world projects, the dialect of your API (REST? GraphQL? Something else?) won't match the JSONPLaceholder dialect. Writing a Data Provider is probably the first thing you'll have to do to make react-admin work. Depending on your API, this can require a few hours of additional work. +Here is the elephant in the room of this tutorial. In real world projects, the dialect of your API (REST? GraphQL? Something else?) won't match the JSONPlaceholder dialect. Writing a Data Provider is probably the first thing you'll have to do to make react-admin work. Depending on your API, this can require a few hours of additional work. React-admin delegates every data query to a Data Provider function. This function must simply return a promise for the result. This gives extreme freedom to map any API dialect, add authentication headers, use endpoints from several domains, etc. From 2f8202808379d50cd8e30cac25c5364508d5e07b Mon Sep 17 00:00:00 2001 From: Tiago Schenkel Date: Thu, 21 Feb 2019 20:49:29 +0100 Subject: [PATCH 30/35] Avoid unnecessary redraws of AutocompleteArrayInputChip --- .../ra-ui-materialui/src/input/AutocompleteArrayInput.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js index 699a03d530d..7f38ce1cc29 100644 --- a/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js +++ b/packages/ra-ui-materialui/src/input/AutocompleteArrayInput.js @@ -102,16 +102,19 @@ const styles = theme => ({ * */ export class AutocompleteArrayInput extends React.Component { + initialInputValue = []; + state = { dirty: false, - inputValue: [], + inputValue: this.initialInputValue, searchText: '', suggestions: [], }; inputEl = null; - getInputValue = inputValue => (inputValue === '' ? [] : inputValue); + getInputValue = inputValue => + inputValue === '' ? this.initialInputValue : inputValue; componentWillMount() { this.setState({ From 20dbe7c50e3ec039677cbd026415ed00c36e6436 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 22 Feb 2019 15:22:10 +0100 Subject: [PATCH 31/35] Prepare changelog for 2.7.2 --- CHANGELOG.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5ced3d90f7..565a0bbafaa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## v2.7.2 + +* Fix JSONPlaceHolder name typo in Tutorial ([2906](https://github.com/marmelab/react-admin/pull/2906)) ([noobling](https://github.com/noobling)) +* Fix `` documentation missing warning ([2904](https://github.com/marmelab/react-admin/pull/2904)) ([mnlbox](https://github.com/mnlbox)) +* Fix prop types for `` ([2898](https://github.com/marmelab/react-admin/pull/2898)) ([kujon](https://github.com/kujon)) +* Fix typo in jsDoc in Tutorial ([2882](https://github.com/marmelab/react-admin/pull/2882)) ([adibnaya](https://github.com/adibnaya)) +* Fix `GET_MANY` in `ra-data-json-server` data provider returns too many results ([2873](https://github.com/marmelab/react-admin/pull/2873)) ([paradoxxxzero](https://github.com/paradoxxxzero)) +* Fix import path typo in Unit Testing documentation ([2872](https://github.com/marmelab/react-admin/pull/2872)) ([mexitalian](https://github.com/mexitalian)) +* Fix `` throws an error when receiving an empty value ([2861](https://github.com/marmelab/react-admin/pull/2861)) ([tiagoschenkel](https://github.com/tiagoschenkel)) +* Fix `` shows choices in a wrong position when input element moves to another location ([2860](https://github.com/marmelab/react-admin/pull/2860)) ([tiagoschenkel](https://github.com/tiagoschenkel)) +* Migrate ra-core controllers to TypeScript ([2881](https://github.com/marmelab/react-admin/pull/2881)) ([djhi](https://github.com/djhi)) +* Migrate ra-core inference to TypeScript ([2879](https://github.com/marmelab/react-admin/pull/2879)) ([djhi](https://github.com/djhi)) +* Migrate ra-core form to TypeScript ([2878](https://github.com/marmelab/react-admin/pull/2878)) ([djhi](https://github.com/djhi)) +* Migrate ra-core i18n Migration to TypeScript ([2874](https://github.com/marmelab/react-admin/pull/2874)) ([djhi](https://github.com/djhi)) + ## v2.7.1 * Fix typo in `ra-data-graphql-simple` documentation ([2863](https://github.com/marmelab/react-admin/pull/2863)) ([EricTousignant](https://github.com/EricTousignant)) From b2f1a6dc743c676f64578956a728ca1f9ea3aa28 Mon Sep 17 00:00:00 2001 From: fzaninotto Date: Fri, 22 Feb 2019 15:22:27 +0100 Subject: [PATCH 32/35] v2.7.2 --- lerna.json | 2 +- packages/ra-core/package.json | 2 +- packages/ra-data-json-server/package.json | 4 ++-- packages/ra-data-simple-rest/package.json | 4 ++-- packages/ra-ui-materialui/package.json | 4 ++-- packages/react-admin/package.json | 6 +++--- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lerna.json b/lerna.json index e112f97d506..c29946b6ed7 100644 --- a/lerna.json +++ b/lerna.json @@ -4,5 +4,5 @@ "examples/data-generator", "packages/*" ], - "version": "2.7.1" + "version": "2.7.2" } diff --git a/packages/ra-core/package.json b/packages/ra-core/package.json index 2a75f043672..85ccdfc0fea 100644 --- a/packages/ra-core/package.json +++ b/packages/ra-core/package.json @@ -1,6 +1,6 @@ { "name": "ra-core", - "version": "2.7.1", + "version": "2.7.2", "description": "Core components of react-admin, a frontend Framework for building admin applications on top of REST services, using ES6, React", "files": [ "*.md", diff --git a/packages/ra-data-json-server/package.json b/packages/ra-data-json-server/package.json index 9b41ecc265b..47e3a472b16 100644 --- a/packages/ra-data-json-server/package.json +++ b/packages/ra-data-json-server/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-json-server", - "version": "2.7.1", + "version": "2.7.2", "description": "JSON Server data provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "~5.1.1", - "ra-core": "^2.7.1" + "ra-core": "^2.7.2" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-data-simple-rest/package.json b/packages/ra-data-simple-rest/package.json index 54f3f33f6d7..3a75cd222b8 100644 --- a/packages/ra-data-simple-rest/package.json +++ b/packages/ra-data-simple-rest/package.json @@ -1,6 +1,6 @@ { "name": "ra-data-simple-rest", - "version": "2.7.1", + "version": "2.7.2", "description": "Simple REST data provider for react-admin", "main": "lib/index.js", "module": "esm/index.js", @@ -26,7 +26,7 @@ }, "dependencies": { "query-string": "~5.1.1", - "ra-core": "^2.7.1" + "ra-core": "^2.7.2" }, "devDependencies": { "cross-env": "^5.2.0", diff --git a/packages/ra-ui-materialui/package.json b/packages/ra-ui-materialui/package.json index cba229d4b82..c4c49e42360 100644 --- a/packages/ra-ui-materialui/package.json +++ b/packages/ra-ui-materialui/package.json @@ -1,6 +1,6 @@ { "name": "ra-ui-materialui", - "version": "2.7.1", + "version": "2.7.2", "description": "UI Components for react-admin with MaterialUI", "files": [ "*.md", @@ -50,7 +50,7 @@ "material-ui-chip-input": "1.0.0-beta.6 - 1.0.0-beta.8", "papaparse": "^4.1.4", "prop-types": "~15.6.1", - "ra-core": "^2.7.1", + "ra-core": "^2.7.2", "react-autosuggest": "^9.4.2", "react-dropzone": "~4.0.1", "react-headroom": "^2.2.4", diff --git a/packages/react-admin/package.json b/packages/react-admin/package.json index 9fe1dfe6957..0b0ad220060 100644 --- a/packages/react-admin/package.json +++ b/packages/react-admin/package.json @@ -1,6 +1,6 @@ { "name": "react-admin", - "version": "2.7.1", + "version": "2.7.2", "description": "A frontend Framework for building admin applications on top of REST services, using ES6, React and Material UI", "files": [ "*.md", @@ -34,8 +34,8 @@ "react-dom": "^16.3.0" }, "dependencies": { - "ra-core": "^2.7.1", + "ra-core": "^2.7.2", "ra-language-english": "^2.7.0", - "ra-ui-materialui": "^2.7.1" + "ra-ui-materialui": "^2.7.2" } } From cc1dbec041c59b071117a19ce54894e91c5f3095 Mon Sep 17 00:00:00 2001 From: Frank Parejo Date: Wed, 6 Feb 2019 16:08:20 +0100 Subject: [PATCH 33/35] change SET_SORT reducer to accept a order value. --- packages/ra-core/src/controller/ListController.tsx | 2 +- .../ra-core/src/reducer/admin/resource/list/queryReducer.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/ra-core/src/controller/ListController.tsx b/packages/ra-core/src/controller/ListController.tsx index 3aa9f5ab490..a8bbf8a0220 100644 --- a/packages/ra-core/src/controller/ListController.tsx +++ b/packages/ra-core/src/controller/ListController.tsx @@ -306,7 +306,7 @@ export class UnconnectedListController extends Component { ); } - setSort = sort => this.changeParams({ type: SET_SORT, payload: sort }); + setSort = sort => this.changeParams({ type: SET_SORT, payload: { sort } }); setPage = page => this.changeParams({ type: SET_PAGE, payload: page }); diff --git a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts index 14bec195df2..cd008f34b33 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.ts @@ -21,7 +21,7 @@ const queryReducer: Reducer = ( ) => { switch (type) { case SET_SORT: - if (payload === previousState.sort) { + if (payload.sort === previousState.sort) { return { ...previousState, order: oppositeOrder(previousState.order), @@ -31,8 +31,8 @@ const queryReducer: Reducer = ( return { ...previousState, - sort: payload, - order: SORT_ASC, + sort: payload.sort, + order: payload.order || SORT_ASC, page: 1, }; From 34c1adc088383a1c8652a03f54f81c5714db71ee Mon Sep 17 00:00:00 2001 From: Frank Parejo Date: Wed, 6 Feb 2019 16:26:24 +0100 Subject: [PATCH 34/35] impleemnt SET_SORT queryReduucer tests. --- .../admin/resource/list/queryReducer.spec.ts | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts index dbd6e4f9139..c3d3a795f5c 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts @@ -1,5 +1,5 @@ import assert from 'assert'; -import queryReducer from './queryReducer'; +import queryReducer, { SORT_ASC, SORT_DESC } from './queryReducer'; describe('Query Reducer', () => { describe('SET_PAGE action', () => { @@ -66,4 +66,50 @@ describe('Query Reducer', () => { assert.equal(updatedState.page, 1); }); }); + describe('SET_SORT action', () => { + it('should set SORT_ASC order by default when sort value is new', () => { + const updatedState = queryReducer( + {}, + { + type: 'SET_SORT', + payload: { sort: 'foo' }, + } + ); + assert.deepEqual(updatedState, { sort: 'foo', order: SORT_ASC, page: 1 }); + }); + it('should set order by payload.order value when sort value is new', () => { + const updatedState = queryReducer( + {}, + { + type: 'SET_SORT', + payload: { sort: 'foo', order: SORT_DESC }, + } + ); + assert.deepEqual(updatedState, { sort: 'foo', order: SORT_DESC, page: 1 }); + }); + it('should set order with the opposite order sort value is the same llke the previous state', () => { + const updatedState = queryReducer( + { + sort: 'foo', order: SORT_DESC, page: 1 + }, + { + type: 'SET_SORT', + payload: { sort: 'foo' }, + } + ); + assert.deepEqual(updatedState, { sort: 'foo', order: SORT_ASC, page: 1 }); + }); + it('should set order with the opposite order sort value is the same llke the previous state even when we pass a specific order', () => { + const updatedState = queryReducer( + { + sort: 'foo', order: SORT_DESC, page: 1 + }, + { + type: 'SET_SORT', + payload: { sort: 'foo' , order: SORT_DESC }, + } + ); + assert.deepEqual(updatedState, { sort: 'foo', order: SORT_ASC, page: 1 }); + }); + }); }); From 0b764f4ce3c3405847d15a4b5938b121170058ba Mon Sep 17 00:00:00 2001 From: Frank Parejo Date: Wed, 6 Feb 2019 17:07:54 +0100 Subject: [PATCH 35/35] fix some wrong titles from queryReducer tests. --- .../src/reducer/admin/resource/list/queryReducer.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts index c3d3a795f5c..038f54eabda 100644 --- a/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts +++ b/packages/ra-core/src/reducer/admin/resource/list/queryReducer.spec.ts @@ -87,7 +87,7 @@ describe('Query Reducer', () => { ); assert.deepEqual(updatedState, { sort: 'foo', order: SORT_DESC, page: 1 }); }); - it('should set order with the opposite order sort value is the same llke the previous state', () => { + it("should set order as the opposite of the one in previous state when sort hasn't change", () => { const updatedState = queryReducer( { sort: 'foo', order: SORT_DESC, page: 1 @@ -99,7 +99,7 @@ describe('Query Reducer', () => { ); assert.deepEqual(updatedState, { sort: 'foo', order: SORT_ASC, page: 1 }); }); - it('should set order with the opposite order sort value is the same llke the previous state even when we pass a specific order', () => { + it("should set order as the opposite of the one in previous state even if order is specified in the payload when sort hasn't change", () => { const updatedState = queryReducer( { sort: 'foo', order: SORT_DESC, page: 1