Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add types for button components #4071

Merged
merged 5 commits into from
Nov 28, 2019
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/ra-core/src/dataProvider/useMutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ export interface MutationOptions {
}

export type UseMutationValue = [
(query: Partial<Mutation>, options?: Partial<MutationOptions>) => void,
(query?: Partial<Mutation>, options?: Partial<MutationOptions>) => void,
{
data?: any;
total?: number;
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/sideEffect/redirection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/ra-core/src/sideEffect/useRedirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import React from 'react';
import React, { FC, ReactElement } from 'react';
import PropTypes from 'prop-types';
import {
Button as MuiButton,
Tooltip,
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';

Expand Down Expand Up @@ -35,7 +38,20 @@ const useStyles = makeStyles(
{ 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
*
* <Button label="Edit" color="secondary" onClick={doEdit}>
* <ContentCreate />
* </Button>
*
*/
const Button: FC<ButtonProps> = ({
alignIcon = 'left',
children,
classes: classesOverride,
Expand All @@ -48,7 +64,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 ? (
Expand Down Expand Up @@ -105,12 +123,25 @@ const Button = ({
);
};

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']),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import expect from 'expect';
import { shallow } from 'enzyme';
import React from 'react';

Expand All @@ -6,7 +7,7 @@ import { CloneButton } from './CloneButton';
describe('<CloneButton />', () => {
it('should pass a clone of the record in the location state', () => {
const wrapper = shallow(
<CloneButton record={{ id: 123, foo: 'bar' }} />
<CloneButton record={{ id: 123, foo: 'bar' }} basePath="" />
);

expect(wrapper.prop('to')).toEqual(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
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';

// 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
Expand All @@ -21,21 +22,27 @@ const sanitizeRestProps = ({
saving,
submitOnEnter,
...rest
}) => rest;
}: any) => rest;

export const CloneButton = ({
export const CloneButton: FC<CloneButtonProps> = ({
basePath = '',
label = 'ra.action.clone',
record = {},
record,
icon = <Queue />,
...rest
}) => (
<Button
component={Link}
to={{
pathname: `${basePath}/create`,
search: stringify({ source: JSON.stringify(omitId(record)) }),
}}
to={
record
? {
pathname: `${basePath}/create`,
search: stringify({
source: JSON.stringify(omitId(record)),
}),
}
: `${basePath}/create`
}
label={label}
onClick={stopPropagation}
{...sanitizeRestProps(rest)}
Expand All @@ -44,18 +51,23 @@ export const CloneButton = ({
</Button>
);

interface Props {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably extract the two first properties in a reusable interface for all our components

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code is hard enough to read like that, I prefer a bit of duplication to a lot of indirection.

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) ||
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
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 => ({
Expand All @@ -28,7 +28,7 @@ const useStyles = makeStyles(
{ name: 'RaCreateButton' }
);

const CreateButton = ({
const CreateButton: FC<CreateButtonProps> = ({
basePath = '',
className,
classes: classesOverride,
Expand All @@ -38,15 +38,17 @@ const CreateButton = ({
}) => {
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 ? (
<Fab
component={Link}
color="primary"
className={classnames(classes.floating, className)}
to={`${basePath}/create`}
aria-label={label && translate(label)}
{...rest}
{...rest as any}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to disable type checking here because of a material-ui bug: Button doesn't recognize the component prop :/

See mui/material-ui#15827

>
{icon}
</Fab>
Expand All @@ -56,20 +58,26 @@ const CreateButton = ({
to={`${basePath}/create`}
className={className}
label={label}
{...rest}
{...rest as any}
>
{icon}
</Button>
);
};

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']);
Expand Down
31 changes: 0 additions & 31 deletions packages/ra-ui-materialui/src/button/DeleteButton.js

This file was deleted.

57 changes: 57 additions & 0 deletions packages/ra-ui-materialui/src/button/DeleteButton.tsx
Original file line number Diff line number Diff line change
@@ -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<DeleteButtonProps> = ({ undoable, ...props }) =>
undoable ? (
<DeleteWithUndoButton {...props} />
) : (
<DeleteWithConfirmButton {...props} />
);

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 DeleteWithUndoButtonutton
handleSubmit?: (event?: SyntheticEvent<HTMLFormElement>) => Promise<Object>;
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;
Loading