diff --git a/packages/ra-core/src/dataProvider/useMutation.ts b/packages/ra-core/src/dataProvider/useMutation.ts index bce22f584d2..c5f6801c488 100644 --- a/packages/ra-core/src/dataProvider/useMutation.ts +++ b/packages/ra-core/src/dataProvider/useMutation.ts @@ -205,7 +205,7 @@ export interface MutationOptions { } export type UseMutationValue = [ - (query: Partial, options?: Partial) => void, + (query?: Partial, options?: Partial) => void, { data?: any; total?: number; diff --git a/packages/ra-core/src/sideEffect/redirection.ts b/packages/ra-core/src/sideEffect/redirection.ts index 457f483c879..5cfab9d9f02 100644 --- a/packages/ra-core/src/sideEffect/redirection.ts +++ b/packages/ra-core/src/sideEffect/redirection.ts @@ -11,7 +11,7 @@ type RedirectToFunction = ( data: any ) => string; -export type RedirectionSideEffect = string | false | RedirectToFunction; +export type RedirectionSideEffect = string | boolean | RedirectToFunction; interface ActionWithSideEffect { type: string; diff --git a/packages/ra-core/src/sideEffect/useRedirect.ts b/packages/ra-core/src/sideEffect/useRedirect.ts index a2d1c828dd0..e245c70301f 100644 --- a/packages/ra-core/src/sideEffect/useRedirect.ts +++ b/packages/ra-core/src/sideEffect/useRedirect.ts @@ -12,7 +12,7 @@ type RedirectToFunction = ( data?: Record ) => string; -export type RedirectionSideEffect = string | false | RedirectToFunction; +export type RedirectionSideEffect = string | boolean | RedirectToFunction; /** * Hook for Redirection Side Effect diff --git a/packages/ra-ui-materialui/src/button/Button.js b/packages/ra-ui-materialui/src/button/Button.tsx similarity index 72% rename from packages/ra-ui-materialui/src/button/Button.js rename to packages/ra-ui-materialui/src/button/Button.tsx index 95f4501238d..54349d21329 100644 --- a/packages/ra-ui-materialui/src/button/Button.js +++ b/packages/ra-ui-materialui/src/button/Button.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { Button as MuiButton, @@ -6,36 +6,27 @@ import { IconButton, useMediaQuery, makeStyles, + PropTypes as MuiPropTypes, } from '@material-ui/core'; +import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button'; +import { Theme } from '@material-ui/core'; import classnames from 'classnames'; import { useTranslate } from 'ra-core'; -const useStyles = makeStyles( - { - button: { - display: 'inline-flex', - alignItems: 'center', - }, - label: { - paddingLeft: '0.5em', - }, - labelRightIcon: { - paddingRight: '0.5em', - }, - smallIcon: { - fontSize: 20, - }, - mediumIcon: { - fontSize: 22, - }, - largeIcon: { - fontSize: 24, - }, - }, - { name: 'RaButton' } -); - -const Button = ({ +/** + * A generic Button with side icon. Only the icon is displayed on small screens. + * + * The component translates the label. Pass the icon as child. + * The icon displays on the left side of the button by default. Set alignIcon prop to 'right' to inverse. + * + * @example + * + * + * + */ +const Button: FC = ({ alignIcon = 'left', children, classes: classesOverride, @@ -48,7 +39,9 @@ const Button = ({ }) => { const translate = useTranslate(); const classes = useStyles({ classes: classesOverride }); - const isXSmall = useMediaQuery(theme => theme.breakpoints.down('xs')); + const isXSmall = useMediaQuery((theme: Theme) => + theme.breakpoints.down('xs') + ); return isXSmall ? ( label && !disabled ? ( @@ -105,12 +98,50 @@ const Button = ({ ); }; +const useStyles = makeStyles( + { + button: { + display: 'inline-flex', + alignItems: 'center', + }, + label: { + paddingLeft: '0.5em', + }, + labelRightIcon: { + paddingRight: '0.5em', + }, + smallIcon: { + fontSize: 20, + }, + mediumIcon: { + fontSize: 22, + }, + largeIcon: { + fontSize: 24, + }, + }, + { name: 'RaButton' } +); + +interface Props { + alignIcon?: 'left' | 'right'; + children?: ReactElement; + classes?: object; + className?: string; + color?: MuiPropTypes.Color; + disabled?: boolean; + label?: string; + size?: 'small' | 'medium' | 'large'; +} + +export type ButtonProps = Props & MuiButtonProps; + Button.propTypes = { - alignIcon: PropTypes.string, + alignIcon: PropTypes.oneOf(['left', 'right']), children: PropTypes.element, classes: PropTypes.object, className: PropTypes.string, - color: PropTypes.string, + color: PropTypes.oneOf(['default', 'inherit', 'primary', 'secondary']), disabled: PropTypes.bool, label: PropTypes.string, size: PropTypes.oneOf(['small', 'medium', 'large']), diff --git a/packages/ra-ui-materialui/src/button/CloneButton.spec.js b/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx similarity index 81% rename from packages/ra-ui-materialui/src/button/CloneButton.spec.js rename to packages/ra-ui-materialui/src/button/CloneButton.spec.tsx index 848e789e3a7..c786f58f6c3 100644 --- a/packages/ra-ui-materialui/src/button/CloneButton.spec.js +++ b/packages/ra-ui-materialui/src/button/CloneButton.spec.tsx @@ -1,3 +1,4 @@ +import expect from 'expect'; import { shallow } from 'enzyme'; import React from 'react'; @@ -6,7 +7,7 @@ import { CloneButton } from './CloneButton'; describe('', () => { it('should pass a clone of the record in the location state', () => { const wrapper = shallow( - + ); expect(wrapper.prop('to')).toEqual( diff --git a/packages/ra-ui-materialui/src/button/CloneButton.js b/packages/ra-ui-materialui/src/button/CloneButton.tsx similarity index 58% rename from packages/ra-ui-materialui/src/button/CloneButton.js rename to packages/ra-ui-materialui/src/button/CloneButton.tsx index b5103ce169a..89427b90c85 100644 --- a/packages/ra-ui-materialui/src/button/CloneButton.js +++ b/packages/ra-ui-materialui/src/button/CloneButton.tsx @@ -1,16 +1,46 @@ -import React from 'react'; +import React, { FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; import shouldUpdate from 'recompose/shouldUpdate'; import Queue from '@material-ui/icons/Queue'; import { Link } from 'react-router-dom'; import { stringify } from 'query-string'; +import { Record } from 'ra-core'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; + +export const CloneButton: FC = ({ + basePath = '', + label = 'ra.action.clone', + record, + icon = defaultIcon, + ...rest +}) => ( + +); + +const defaultIcon = ; // useful to prevent click bubbling in a datagrid with rowClick const stopPropagation = e => e.stopPropagation(); -const omitId = ({ id, ...rest }) => rest; +const omitId = ({ id, ...rest }: Record) => rest; const sanitizeRestProps = ({ // the next 6 props are injected by Toolbar @@ -21,41 +51,25 @@ const sanitizeRestProps = ({ saving, submitOnEnter, ...rest -}) => rest; +}: any) => rest; -export const CloneButton = ({ - basePath = '', - label = 'ra.action.clone', - record = {}, - icon = , - ...rest -}) => ( - -); +interface Props { + basePath?: string; + record?: Record; + icon?: ReactElement; +} + +export type CloneButtonProps = Props & ButtonProps; CloneButton.propTypes = { basePath: PropTypes.string, - className: PropTypes.string, - classes: PropTypes.object, - label: PropTypes.string, - record: PropTypes.object, icon: PropTypes.element, + label: PropTypes.string, + record: PropTypes.any, }; const enhance = shouldUpdate( - (props, nextProps) => - props.translate !== nextProps.translate || + (props: Props, nextProps: Props) => (props.record && nextProps.record && props.record !== nextProps.record) || diff --git a/packages/ra-ui-materialui/src/button/CreateButton.js b/packages/ra-ui-materialui/src/button/CreateButton.tsx similarity index 75% rename from packages/ra-ui-materialui/src/button/CreateButton.js rename to packages/ra-ui-materialui/src/button/CreateButton.tsx index e34c4a5e971..821bd473c9e 100644 --- a/packages/ra-ui-materialui/src/button/CreateButton.js +++ b/packages/ra-ui-materialui/src/button/CreateButton.tsx @@ -1,44 +1,27 @@ -import React from 'react'; +import React, { FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; import onlyUpdateForKeys from 'recompose/onlyUpdateForKeys'; -import { Fab, makeStyles, useMediaQuery } from '@material-ui/core'; +import { Fab, makeStyles, useMediaQuery, Theme } from '@material-ui/core'; import ContentAdd from '@material-ui/icons/Add'; import classnames from 'classnames'; import { Link } from 'react-router-dom'; import { useTranslate } from 'ra-core'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -const useStyles = makeStyles( - theme => ({ - floating: { - color: theme.palette.getContrastText(theme.palette.primary.main), - margin: 0, - top: 'auto', - right: 20, - bottom: 60, - left: 'auto', - position: 'fixed', - zIndex: 1000, - }, - floatingLink: { - color: 'inherit', - }, - }), - { name: 'RaCreateButton' } -); - -const CreateButton = ({ +const CreateButton: FC = ({ basePath = '', className, classes: classesOverride, label = 'ra.action.create', - icon = , + icon = defaultIcon, ...rest }) => { const classes = useStyles({ classes: classesOverride }); const translate = useTranslate(); - const isSmall = useMediaQuery(theme => theme.breakpoints.down('sm')); + const isSmall = useMediaQuery((theme: Theme) => + theme.breakpoints.down('sm') + ); return isSmall ? ( {icon} @@ -56,20 +39,47 @@ const CreateButton = ({ to={`${basePath}/create`} className={className} label={label} - {...rest} + {...rest as any} > {icon} ); }; +const defaultIcon = ; + +const useStyles = makeStyles( + theme => ({ + floating: { + color: theme.palette.getContrastText(theme.palette.primary.main), + margin: 0, + top: 'auto', + right: 20, + bottom: 60, + left: 'auto', + position: 'fixed', + zIndex: 1000, + }, + floatingLink: { + color: 'inherit', + }, + }), + { name: 'RaCreateButton' } +); + +interface Props { + basePath?: string; + icon?: ReactElement; +} + +export type CreateButtonProps = Props & ButtonProps; + CreateButton.propTypes = { basePath: PropTypes.string, - className: PropTypes.string, classes: PropTypes.object, - label: PropTypes.string, - size: PropTypes.string, + className: PropTypes.string, icon: PropTypes.element, + label: PropTypes.string, }; const enhance = onlyUpdateForKeys(['basePath', 'label', 'translate']); diff --git a/packages/ra-ui-materialui/src/button/DeleteButton.js b/packages/ra-ui-materialui/src/button/DeleteButton.js deleted file mode 100644 index 7eb9340dda7..00000000000 --- a/packages/ra-ui-materialui/src/button/DeleteButton.js +++ /dev/null @@ -1,31 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import DeleteWithUndoButton from './DeleteWithUndoButton'; -import DeleteWithConfirmButton from './DeleteWithConfirmButton'; - -const DeleteButton = ({ undoable, ...props }) => - undoable ? ( - - ) : ( - - ); - -DeleteButton.propTypes = { - basePath: PropTypes.string, - label: PropTypes.string, - record: PropTypes.object, - redirect: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.bool, - PropTypes.func, - ]), - resource: PropTypes.string, - undoable: PropTypes.bool, - icon: PropTypes.element, -}; - -DeleteButton.defaultProps = { - undoable: true, -}; - -export default DeleteButton; diff --git a/packages/ra-ui-materialui/src/button/DeleteButton.tsx b/packages/ra-ui-materialui/src/button/DeleteButton.tsx new file mode 100644 index 00000000000..612a5c68605 --- /dev/null +++ b/packages/ra-ui-materialui/src/button/DeleteButton.tsx @@ -0,0 +1,57 @@ +import React, { FC, ReactElement, SyntheticEvent } from 'react'; +import PropTypes from 'prop-types'; +import { Record, RedirectionSideEffect } from 'ra-core'; + +import { ButtonProps } from './Button'; +import DeleteWithUndoButton from './DeleteWithUndoButton'; +import DeleteWithConfirmButton from './DeleteWithConfirmButton'; + +const DeleteButton: FC = ({ undoable, ...props }) => + undoable ? ( + + ) : ( + + ); + +interface Props { + basePath?: string; + classes?: object; + className?: string; + icon?: ReactElement; + label?: string; + onClick?: (e: MouseEvent) => void; + record?: Record; + redirect?: RedirectionSideEffect; + resource?: string; + // May be injected by Toolbar + handleSubmit?: (event?: SyntheticEvent) => Promise; + handleSubmitWithRedirect?: (redirect?: RedirectionSideEffect) => void; + invalid?: boolean; + pristine?: boolean; + saving?: boolean; + submitOnEnter?: boolean; + undoable?: boolean; +} + +export type DeleteButtonProps = Props & ButtonProps; + +DeleteButton.propTypes = { + basePath: PropTypes.string, + label: PropTypes.string, + record: PropTypes.any, + // @ts-ignore + redirect: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.bool, + PropTypes.func, + ]), + resource: PropTypes.string, + undoable: PropTypes.bool, + icon: PropTypes.element, +}; + +DeleteButton.defaultProps = { + undoable: true, +}; + +export default DeleteButton; diff --git a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.js b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx similarity index 73% rename from packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.js rename to packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx index 448a6362c62..efcd8e63e47 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.js +++ b/packages/ra-ui-materialui/src/button/DeleteWithConfirmButton.tsx @@ -1,4 +1,11 @@ -import React, { Fragment, useState, useCallback } from 'react'; +import React, { + Fragment, + useState, + useCallback, + FC, + ReactElement, + SyntheticEvent, +} from 'react'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import { fade } from '@material-ui/core/styles/colorManipulator'; @@ -12,56 +19,25 @@ import { useNotify, useRedirect, CRUD_DELETE, + Record, + RedirectionSideEffect, } from 'ra-core'; import Confirm from '../layout/Confirm'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -const sanitizeRestProps = ({ - basePath, - classes, - filterValues, - handleSubmit, - handleSubmitWithRedirect, - invalid, - label, - pristine, - resource, - saving, - selectedIds, - submitOnEnter, - redirect, - ...rest -}) => rest; - -const useStyles = makeStyles( - theme => ({ - deleteButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: fade(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - }, - }), - { name: 'RaDeleteWithConfirmButton' } -); - -const DeleteWithConfirmButton = ({ +const DeleteWithConfirmButton: FC = ({ basePath, classes: classesOverride, className, confirmTitle = 'ra.message.delete_title', confirmContent = 'ra.message.delete_content', - icon, + icon = defaultIcon, label = 'ra.action.delete', onClick, record, resource, - redirect: redirectTo, + redirect: redirectTo = 'list', ...rest }) => { const [open, setOpen] = useState(false); @@ -103,12 +79,15 @@ const DeleteWithConfirmButton = ({ e.stopPropagation(); }; - const handleDelete = useCallback(() => { - deleteOne(); - if (typeof onClick === 'function') { - onClick(); - } - }, [deleteOne, onClick]); + const handleDelete = useCallback( + event => { + deleteOne(); + if (typeof onClick === 'function') { + onClick(event); + } + }, + [deleteOne, onClick] + ); return ( @@ -147,6 +126,60 @@ const DeleteWithConfirmButton = ({ ); }; +const defaultIcon = ; + +const sanitizeRestProps = ({ + handleSubmit, + handleSubmitWithRedirect, + invalid, + label, + pristine, + saving, + submitOnEnter, + undoable, + ...rest +}: DeleteWithConfirmButtonProps) => rest; + +const useStyles = makeStyles( + theme => ({ + deleteButton: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: fade(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + }, + }), + { name: 'RaDeleteWithConfirmButton' } +); + +interface Props { + basePath?: string; + classes?: object; + className?: string; + confirmTitle?: string; + confirmContent?: string; + icon?: ReactElement; + label?: string; + onClick?: (e: MouseEvent) => void; + record?: Record; + redirect?: RedirectionSideEffect; + resource?: string; + // May be injected by Toolbar - sanitized in DeleteWithConfirButton + handleSubmit?: (event?: SyntheticEvent) => Promise; + handleSubmitWithRedirect?: (redirect?: RedirectionSideEffect) => void; + invalid?: boolean; + pristine?: boolean; + saving?: boolean; + submitOnEnter?: boolean; + undoable?: boolean; +} + +type DeleteWithConfirmButtonProps = Props & ButtonProps; + DeleteWithConfirmButton.propTypes = { basePath: PropTypes.string, classes: PropTypes.object, @@ -154,7 +187,7 @@ DeleteWithConfirmButton.propTypes = { confirmTitle: PropTypes.string, confirmContent: PropTypes.string, label: PropTypes.string, - record: PropTypes.object, + record: PropTypes.any, redirect: PropTypes.oneOfType([ PropTypes.string, PropTypes.bool, @@ -164,9 +197,4 @@ DeleteWithConfirmButton.propTypes = { icon: PropTypes.element, }; -DeleteWithConfirmButton.defaultProps = { - redirect: 'list', - icon: , -}; - export default DeleteWithConfirmButton; diff --git a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx similarity index 74% rename from packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js rename to packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx index eaeb9b35da1..bae58776929 100644 --- a/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.js +++ b/packages/ra-ui-materialui/src/button/DeleteWithUndoButton.tsx @@ -1,4 +1,4 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, FC, ReactElement, SyntheticEvent } from 'react'; import PropTypes from 'prop-types'; import { makeStyles } from '@material-ui/core/styles'; import { fade } from '@material-ui/core/styles/colorManipulator'; @@ -10,54 +10,22 @@ import { useNotify, useRedirect, CRUD_DELETE, + Record, + RedirectionSideEffect, } from 'ra-core'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -export const sanitizeRestProps = ({ - basePath, - classes, - filterValues, - handleSubmit, - handleSubmitWithRedirect, - invalid, - label, - pristine, - resource, - saving, - selectedIds, - undoable, - redirect, - submitOnEnter, - ...rest -}) => rest; - -const useStyles = makeStyles( - theme => ({ - deleteButton: { - color: theme.palette.error.main, - '&:hover': { - backgroundColor: fade(theme.palette.error.main, 0.12), - // Reset on mouse devices - '@media (hover: none)': { - backgroundColor: 'transparent', - }, - }, - }, - }), - { name: 'RaDeleteWithUndoButton' } -); - -const DeleteWithUndoButton = ({ +const DeleteWithUndoButton: FC = ({ label = 'ra.action.delete', classes: classesOverride, className, - icon, + icon = defaultIcon, onClick, resource, record, basePath, - redirect: redirectTo, + redirect: redirectTo = 'list', ...rest }) => { const classes = useStyles({ classes: classesOverride }); @@ -96,7 +64,7 @@ const DeleteWithUndoButton = ({ event.stopPropagation(); deleteOne(); if (typeof onClick === 'function') { - onClick(); + onClick(event); } }, [deleteOne, onClick] @@ -120,12 +88,67 @@ const DeleteWithUndoButton = ({ ); }; +export const sanitizeRestProps = ({ + classes, + handleSubmit, + handleSubmitWithRedirect, + invalid, + label, + pristine, + resource, + saving, + undoable, + redirect, + submitOnEnter, + ...rest +}: DeleteWithUndoButtonProps) => rest; + +const useStyles = makeStyles( + theme => ({ + deleteButton: { + color: theme.palette.error.main, + '&:hover': { + backgroundColor: fade(theme.palette.error.main, 0.12), + // Reset on mouse devices + '@media (hover: none)': { + backgroundColor: 'transparent', + }, + }, + }, + }), + { name: 'RaDeleteWithUndoButton' } +); + +interface Props { + basePath?: string; + classes?: object; + className?: string; + icon?: ReactElement; + label?: string; + onClick?: (e: MouseEvent) => void; + record?: Record; + redirect?: RedirectionSideEffect; + resource?: string; + // May be injected by Toolbar - sanitized in DeleteWithUndoButton + handleSubmit?: (event?: SyntheticEvent) => Promise; + handleSubmitWithRedirect?: (redirect?: RedirectionSideEffect) => void; + invalid?: boolean; + pristine?: boolean; + saving?: boolean; + submitOnEnter?: boolean; + undoable?: boolean; +} + +const defaultIcon = ; + +export type DeleteWithUndoButtonProps = Props & ButtonProps; + DeleteWithUndoButton.propTypes = { basePath: PropTypes.string, classes: PropTypes.object, className: PropTypes.string, label: PropTypes.string, - record: PropTypes.object, + record: PropTypes.any, redirect: PropTypes.oneOfType([ PropTypes.string, PropTypes.bool, @@ -135,10 +158,4 @@ DeleteWithUndoButton.propTypes = { icon: PropTypes.element, }; -DeleteWithUndoButton.defaultProps = { - redirect: 'list', - undoable: true, - icon: , -}; - export default DeleteWithUndoButton; diff --git a/packages/ra-ui-materialui/src/button/EditButton.js b/packages/ra-ui-materialui/src/button/EditButton.js deleted file mode 100644 index df3162899e6..00000000000 --- a/packages/ra-ui-materialui/src/button/EditButton.js +++ /dev/null @@ -1,39 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ContentCreate from '@material-ui/icons/Create'; -import { Link } from 'react-router-dom'; -import { linkToRecord } from 'ra-core'; - -import Button from './Button'; - -// useful to prevent click bubbling in a datagrid with rowClick -const stopPropagation = e => e.stopPropagation(); - -const EditButton = ({ - basePath = '', - label = 'ra.action.edit', - record = {}, - icon = , - ...rest -}) => ( - -); - -EditButton.propTypes = { - basePath: PropTypes.string, - className: PropTypes.string, - classes: PropTypes.object, - label: PropTypes.string, - record: PropTypes.object, - icon: PropTypes.element, -}; - -export default EditButton; diff --git a/packages/ra-ui-materialui/src/button/EditButton.tsx b/packages/ra-ui-materialui/src/button/EditButton.tsx new file mode 100644 index 00000000000..638886a5384 --- /dev/null +++ b/packages/ra-ui-materialui/src/button/EditButton.tsx @@ -0,0 +1,48 @@ +import React, { FC, ReactElement } from 'react'; +import PropTypes from 'prop-types'; +import ContentCreate from '@material-ui/icons/Create'; +import { ButtonProps as MuiButtonProps } from '@material-ui/core/Button'; +import { Link } from 'react-router-dom'; +import { linkToRecord, Record } from 'ra-core'; + +import Button, { ButtonProps } from './Button'; + +const EditButton: FC = ({ + basePath = '', + label = 'ra.action.edit', + record, + icon = defaultIcon, + ...rest +}) => ( + +); + +const defaultIcon = ; + +// useful to prevent click bubbling in a datagrid with rowClick +const stopPropagation = e => e.stopPropagation(); + +interface Props { + basePath?: string; + record?: Record; + icon?: ReactElement; +} + +export type EditButtonProps = Props & ButtonProps & MuiButtonProps; + +EditButton.propTypes = { + basePath: PropTypes.string, + icon: PropTypes.element, + label: PropTypes.string, + record: PropTypes.any, +}; + +export default EditButton; diff --git a/packages/ra-ui-materialui/src/button/ExportButton.spec.js b/packages/ra-ui-materialui/src/button/ExportButton.spec.ts similarity index 97% rename from packages/ra-ui-materialui/src/button/ExportButton.spec.js rename to packages/ra-ui-materialui/src/button/ExportButton.spec.ts index a4c332ac739..81079fca0ac 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.spec.js +++ b/packages/ra-ui-materialui/src/button/ExportButton.spec.ts @@ -1,3 +1,5 @@ +import expect from 'expect'; + import { getRelatedIds } from './ExportButton'; describe('ExportButton', () => { diff --git a/packages/ra-ui-materialui/src/button/ExportButton.tsx b/packages/ra-ui-materialui/src/button/ExportButton.tsx index 23b10073a34..5c22b757439 100644 --- a/packages/ra-ui-materialui/src/button/ExportButton.tsx +++ b/packages/ra-ui-materialui/src/button/ExportButton.tsx @@ -1,7 +1,6 @@ import React, { useCallback, FunctionComponent } from 'react'; import PropTypes from 'prop-types'; import DownloadIcon from '@material-ui/icons/GetApp'; -import { ButtonProps } from '@material-ui/core/Button'; import { downloadCSV, useDataProvider, @@ -11,9 +10,70 @@ import { } from 'ra-core'; import jsonExport from 'jsonexport/dist'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -const sanitizeRestProps = ({ basePath, ...rest }: any) => rest; +const ExportButton: FunctionComponent = ({ + exporter, + sort, + filter = defaultFilter, + maxResults = 1000, + resource, + onClick, + label = 'ra.action.export', + icon = defaultIcon, + ...rest +}) => { + const dataProvider = useDataProvider(); + const notify = useNotify(); + const handleClick = useCallback( + event => { + dataProvider + .getList(resource, { + sort, + filter, + pagination: { page: 1, perPage: maxResults }, + }) + .then(({ data }) => + exporter + ? exporter( + data, + fetchRelatedRecords(dataProvider), + dataProvider + ) + : jsonExport(data, (err, csv) => + downloadCSV(csv, resource) + ) + ) + .catch(error => { + console.error(error); + notify('ra.notification.http_error', 'warning'); + }); + if (typeof onClick === 'function') { + onClick(event); + } + }, + [ + dataProvider, + exporter, + filter, + maxResults, + notify, + onClick, + resource, + sort, + ] + ); + + return ( + + ); +}; /** * Extracts, aggregates and deduplicates the ids of related records @@ -68,10 +128,19 @@ const fetchRelatedRecords = dataProvider => (data, field, resource) => }, {}) ); -const DefaultIcon = ; +const defaultIcon = ; const defaultFilter = {}; +const sanitizeRestProps = ({ + basePath, + ...rest +}: Omit< + ExportButtonProps, + 'exporter' | 'sort' | 'filter' | 'maxResults' | 'resource' | 'label' +>) => rest; + interface Props { + basePath?: string; exporter?: ( data: any, fetchRelatedRecords: ( @@ -81,78 +150,16 @@ interface Props { ) => Promise, dataProvider: DataProvider ) => Promise; - sort: Sort; filter?: any; - maxResults: number; - resource: string; - onClick?: (e: Event) => void; - label: string; icon?: JSX.Element; - basePath: string; + label?: string; + maxResults?: number; + onClick?: (e: Event) => void; + resource?: string; + sort?: Sort; } -const ExportButton: FunctionComponent = ({ - exporter, - sort, - filter = defaultFilter, - maxResults = 1000, - resource, - onClick, - label = 'ra.action.export', - icon = DefaultIcon, - ...rest -}) => { - const dataProvider = useDataProvider(); - const notify = useNotify(); - const handleClick = useCallback( - event => { - dataProvider - .getList(resource, { - sort, - filter, - pagination: { page: 1, perPage: maxResults }, - }) - .then(({ data }) => - exporter - ? exporter( - data, - fetchRelatedRecords(dataProvider), - dataProvider - ) - : jsonExport(data, (err, csv) => - downloadCSV(csv, resource) - ) - ) - .catch(error => { - console.error(error); - notify('ra.notification.http_error', 'warning'); - }); - if (typeof onClick === 'function') { - onClick(event); - } - }, - [ - dataProvider, - exporter, - filter, - maxResults, - notify, - onClick, - resource, - sort, - ] - ); - - return ( - - ); -}; +export type ExportButtonProps = Props & ButtonProps; ExportButton.propTypes = { basePath: PropTypes.string, diff --git a/packages/ra-ui-materialui/src/button/ListButton.js b/packages/ra-ui-materialui/src/button/ListButton.tsx similarity index 56% rename from packages/ra-ui-materialui/src/button/ListButton.js rename to packages/ra-ui-materialui/src/button/ListButton.tsx index 3a0b07e67b6..c306ebe150e 100644 --- a/packages/ra-ui-materialui/src/button/ListButton.js +++ b/packages/ra-ui-materialui/src/button/ListButton.tsx @@ -1,25 +1,34 @@ -import React from 'react'; +import React, { FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; import ActionList from '@material-ui/icons/List'; import { Link } from 'react-router-dom'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -const ListButton = ({ +const ListButton: FC = ({ basePath = '', label = 'ra.action.list', - icon = , + icon = defaultIcon, ...rest }) => ( - ); +const defaultIcon = ; + +interface Props { + basePath?: string; + icon?: ReactElement; +} + +export type ListButtonProps = Props & ButtonProps; + ListButton.propTypes = { basePath: PropTypes.string, - label: PropTypes.string, icon: PropTypes.element, + label: PropTypes.string, }; export default ListButton; diff --git a/packages/ra-ui-materialui/src/button/RefreshButton.js b/packages/ra-ui-materialui/src/button/RefreshButton.tsx similarity index 68% rename from packages/ra-ui-materialui/src/button/RefreshButton.js rename to packages/ra-ui-materialui/src/button/RefreshButton.tsx index 90be0025fc3..ab132d98dbb 100644 --- a/packages/ra-ui-materialui/src/button/RefreshButton.js +++ b/packages/ra-ui-materialui/src/button/RefreshButton.tsx @@ -1,14 +1,12 @@ -import React, { useCallback } from 'react'; +import React, { FC, ReactElement, MouseEvent, useCallback } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; import NavigationRefresh from '@material-ui/icons/Refresh'; import { refreshView } from 'ra-core'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -const defaultIcon = ; - -const RefreshButton = ({ +const RefreshButton: FC = ({ label = 'ra.action.refresh', icon = defaultIcon, onClick, @@ -20,7 +18,7 @@ const RefreshButton = ({ event.preventDefault(); dispatch(refreshView()); if (typeof onClick === 'function') { - onClick(); + onClick(event); } }, [dispatch, onClick] @@ -33,9 +31,20 @@ const RefreshButton = ({ ); }; +const defaultIcon = ; + +interface Props { + label?: string; + icon?: ReactElement; + onClick?: (e: MouseEvent) => void; +} + +export type RefreshButtonProps = Props & ButtonProps; + RefreshButton.propTypes = { label: PropTypes.string, icon: PropTypes.element, + onClick: PropTypes.func, }; export default RefreshButton; diff --git a/packages/ra-ui-materialui/src/button/RefreshIconButton.js b/packages/ra-ui-materialui/src/button/RefreshIconButton.tsx similarity index 75% rename from packages/ra-ui-materialui/src/button/RefreshIconButton.js rename to packages/ra-ui-materialui/src/button/RefreshIconButton.tsx index d090868fe93..056b60e292e 100644 --- a/packages/ra-ui-materialui/src/button/RefreshIconButton.js +++ b/packages/ra-ui-materialui/src/button/RefreshIconButton.tsx @@ -1,14 +1,12 @@ -import React, { useCallback } from 'react'; +import React, { useCallback, FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; import { useDispatch } from 'react-redux'; import Tooltip from '@material-ui/core/Tooltip'; -import IconButton from '@material-ui/core/IconButton'; +import IconButton, { IconButtonProps } from '@material-ui/core/IconButton'; import NavigationRefresh from '@material-ui/icons/Refresh'; import { refreshView, useTranslate } from 'ra-core'; -const defaultIcon = ; - -const RefreshIconButton = ({ +const RefreshIconButton: FC = ({ label = 'ra.action.refresh', icon = defaultIcon, onClick, @@ -22,7 +20,7 @@ const RefreshIconButton = ({ event.preventDefault(); dispatch(refreshView()); if (typeof onClick === 'function') { - onClick(); + onClick(event); } }, [dispatch, onClick] @@ -43,6 +41,17 @@ const RefreshIconButton = ({ ); }; +const defaultIcon = ; + +interface Props { + className?: string; + icon?: ReactElement; + label?: string; + onClick?: (e: MouseEvent) => void; +} + +export type RefreshIconProps = Props & IconButtonProps; + RefreshIconButton.propTypes = { className: PropTypes.string, label: PropTypes.string, diff --git a/packages/ra-ui-materialui/src/button/SaveButton.spec.js b/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx similarity index 98% rename from packages/ra-ui-materialui/src/button/SaveButton.spec.js rename to packages/ra-ui-materialui/src/button/SaveButton.spec.tsx index 23814aa9b11..f19160d1ef7 100644 --- a/packages/ra-ui-materialui/src/button/SaveButton.spec.js +++ b/packages/ra-ui-materialui/src/button/SaveButton.spec.tsx @@ -1,5 +1,6 @@ import { render, cleanup, fireEvent } from '@testing-library/react'; import React from 'react'; +import expect from 'expect'; import { TestContext } from 'ra-core'; import SaveButton from './SaveButton'; diff --git a/packages/ra-ui-materialui/src/button/SaveButton.js b/packages/ra-ui-materialui/src/button/SaveButton.tsx similarity index 77% rename from packages/ra-ui-materialui/src/button/SaveButton.js rename to packages/ra-ui-materialui/src/button/SaveButton.tsx index c503422a1cd..bdeadff6fa8 100644 --- a/packages/ra-ui-materialui/src/button/SaveButton.js +++ b/packages/ra-ui-materialui/src/button/SaveButton.tsx @@ -1,47 +1,18 @@ -import React, { cloneElement } from 'react'; +import React, { cloneElement, FC, ReactElement, SyntheticEvent } from 'react'; import PropTypes from 'prop-types'; -import Button from '@material-ui/core/Button'; +import Button, { ButtonProps } from '@material-ui/core/Button'; import CircularProgress from '@material-ui/core/CircularProgress'; import { makeStyles } from '@material-ui/core/styles'; import ContentSave from '@material-ui/icons/Save'; import classnames from 'classnames'; -import { useTranslate, useNotify } from 'ra-core'; +import { + useTranslate, + useNotify, + RedirectionSideEffect, + Record, +} from 'ra-core'; -const useStyles = makeStyles( - theme => ({ - button: { - position: 'relative', - }, - leftIcon: { - marginRight: theme.spacing(1), - }, - icon: { - fontSize: 18, - }, - }), - { name: 'RaSaveButton' } -); - -const sanitizeRestProps = ({ - basePath, - className, - classes, - saving, - label, - invalid, - variant, - handleSubmit, - handleSubmitWithRedirect, - submitOnEnter, - record, - redirect, - resource, - locale, - undoable, - ...rest -}) => rest; - -const SaveButton = ({ +const SaveButton: FC = ({ className, classes: classesOverride = {}, invalid, @@ -51,7 +22,7 @@ const SaveButton = ({ saving, submitOnEnter, variant = 'contained', - icon, + icon = defaultIcon, onClick, handleSubmitWithRedirect, ...rest @@ -106,7 +77,7 @@ const SaveButton = ({ aria-label={displayedLabel} {...sanitizeRestProps(rest)} > - {saving && saving.redirect === redirect ? ( + {saving ? ( ; + +const useStyles = makeStyles( + theme => ({ + button: { + position: 'relative', + }, + leftIcon: { + marginRight: theme.spacing(1), + }, + icon: { + fontSize: 18, + }, + }), + { name: 'RaSaveButton' } +); + +const sanitizeRestProps = ({ + basePath, + handleSubmit, + record, + resource, + undoable, + ...rest +}: SaveButtonProps) => rest; + +interface Props { + classes?: object; + className?: string; + handleSubmitWithRedirect?: (redirect?: RedirectionSideEffect) => void; + icon?: ReactElement; + invalid?: boolean; + label?: string; + onClick?: () => void; + pristine?: boolean; + redirect?: RedirectionSideEffect; + saving?: boolean; + submitOnEnter?: boolean; + variant?: string; + // May be injected by Toolbar - sanitized in SaveButton + basePath?: string; + handleSubmit?: (event?: SyntheticEvent) => Promise; + record?: Record; + resource?: string; + undoable?: boolean; +} + +type SaveButtonProps = Props & ButtonProps; + SaveButton.propTypes = { className: PropTypes.string, classes: PropTypes.object, @@ -134,14 +154,10 @@ SaveButton.propTypes = { PropTypes.bool, PropTypes.func, ]), - saving: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + saving: PropTypes.bool, submitOnEnter: PropTypes.bool, variant: PropTypes.oneOf(['text', 'outlined', 'contained']), icon: PropTypes.element, }; -SaveButton.defaultProps = { - icon: , -}; - export default SaveButton; diff --git a/packages/ra-ui-materialui/src/button/ShowButton.js b/packages/ra-ui-materialui/src/button/ShowButton.tsx similarity index 61% rename from packages/ra-ui-materialui/src/button/ShowButton.js rename to packages/ra-ui-materialui/src/button/ShowButton.tsx index dfe69d00f3e..8ed0d89d16c 100644 --- a/packages/ra-ui-materialui/src/button/ShowButton.js +++ b/packages/ra-ui-materialui/src/button/ShowButton.tsx @@ -1,43 +1,52 @@ -import React from 'react'; +import React, { FC, ReactElement } from 'react'; import PropTypes from 'prop-types'; import shouldUpdate from 'recompose/shouldUpdate'; import ImageEye from '@material-ui/icons/RemoveRedEye'; import { Link } from 'react-router-dom'; -import { linkToRecord } from 'ra-core'; +import { linkToRecord, Record } from 'ra-core'; -import Button from './Button'; +import Button, { ButtonProps } from './Button'; -// useful to prevent click bubbling in a datagrid with rowClick -const stopPropagation = e => e.stopPropagation(); - -const ShowButton = ({ +const ShowButton: FC = ({ basePath = '', label = 'ra.action.show', - record = {}, - icon = , + record, + icon = defaultIcon, ...rest }) => ( ); +const defaultIcon = ; + +// useful to prevent click bubbling in a datagrid with rowClick +const stopPropagation = e => e.stopPropagation(); + +interface Props { + basePath?: string; + record?: Record; + icon?: ReactElement; +} + +export type ShowButtonProps = Props & ButtonProps; + ShowButton.propTypes = { basePath: PropTypes.string, - label: PropTypes.string, - record: PropTypes.object, icon: PropTypes.element, + label: PropTypes.string, + record: PropTypes.any, }; const enhance = shouldUpdate( - (props, nextProps) => - props.translate !== nextProps.translate || + (props: Props, nextProps: Props) => (props.record && nextProps.record && props.record.id !== nextProps.record.id) || diff --git a/packages/ra-ui-materialui/src/form/SimpleForm.js b/packages/ra-ui-materialui/src/form/SimpleForm.js index d25a92089f1..63ca6332b17 100644 --- a/packages/ra-ui-materialui/src/form/SimpleForm.js +++ b/packages/ra-ui-materialui/src/form/SimpleForm.js @@ -177,7 +177,7 @@ SimpleFormView.propTypes = { PropTypes.func, ]), save: PropTypes.func, // the handler defined in the parent, which triggers the REST submission - saving: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + saving: PropTypes.bool, submitOnEnter: PropTypes.bool, toolbar: PropTypes.element, undoable: PropTypes.bool, diff --git a/packages/ra-ui-materialui/src/form/TabbedForm.js b/packages/ra-ui-materialui/src/form/TabbedForm.js index bed8d6920e4..3064858226b 100644 --- a/packages/ra-ui-materialui/src/form/TabbedForm.js +++ b/packages/ra-ui-materialui/src/form/TabbedForm.js @@ -242,7 +242,7 @@ TabbedFormView.propTypes = { ]), resource: PropTypes.string, save: PropTypes.func, // the handler defined in the parent, which triggers the REST submission - saving: PropTypes.oneOfType([PropTypes.object, PropTypes.bool]), + saving: PropTypes.bool, submitOnEnter: PropTypes.bool, tabs: PropTypes.element.isRequired, tabsWithErrors: PropTypes.arrayOf(PropTypes.string), diff --git a/yarn.lock b/yarn.lock index 92b6b56e0b4..f5b307e9712 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4981,14 +4981,6 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" -data-generator-retail@^2.7.0: - version "2.9.5" - resolved "https://registry.yarnpkg.com/data-generator-retail/-/data-generator-retail-2.9.5.tgz#9a1a541f7bc19c00b633b0d17e6b39837e398976" - integrity sha512-scx3c91hhTMxFIvNgAO2Y2Xor4eIR8FfZ7LBCHVlsUt7DEIUvH6juHIVjT6ytzQjwBuR03UzEOlZpIMim1+KqQ== - dependencies: - date-fns "~1.29.0" - faker "^4.1.0" - data-urls@^1.0.0, data-urls@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/data-urls/-/data-urls-1.1.0.tgz#15ee0582baa5e22bb59c77140da8f9c76963bbfe"