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

[RFR] Add emptyText prop to show a fixed string when the value of a field is null #4413

Merged
merged 14 commits into from
Feb 20, 2020
Merged
1 change: 1 addition & 0 deletions docs/Fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ All field components accept the following attributes:
* `formClassName`: A class name (usually generated by JSS) to customize the look and feel of the field container when it is used inside `<SimpleForm>` or `<TabbedForm>`.
* `addLabel`: Defines the visibility of the label when the field is not in a `Datagrid`. Default value is `true`.
* `textAlign`: Defines the text alignment inside a cell. Supports `left` (the default) and `right`.
* `emptyText`: Defines a text to be shown when a field has no value.

{% raw %}
```jsx
Expand Down
13 changes: 13 additions & 0 deletions packages/ra-ui-materialui/src/field/BooleanField.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,19 @@ describe('<BooleanField />', () => {
expect(queryByTitle('ra.boolean.false')).toBeNull();
});

it('should display the emptyText when is present and the value is null', () => {
const { queryByTitle, queryByText } = render(
<BooleanField
{...defaultProps}
record={{ published: null }}
emptyText="NA"
/>
);
expect(queryByTitle('ra.boolean.true')).toBeNull();
expect(queryByTitle('ra.boolean.false')).toBeNull();
expect(queryByText('NA')).not.toBeNull();
});

it('should use custom className', () => {
const { container } = render(
<BooleanField
Expand Down
5 changes: 4 additions & 1 deletion packages/ra-ui-materialui/src/field/BooleanField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const BooleanField: FunctionComponent<
> = ({
className,
classes: classesOverride,
emptyText,
source,
record = {},
valueLabelTrue,
Expand Down Expand Up @@ -72,7 +73,9 @@ export const BooleanField: FunctionComponent<
variant="body2"
className={className}
{...sanitizeRestProps(rest)}
/>
>
{emptyText}
</Typography>
);
};

Expand Down
13 changes: 13 additions & 0 deletions packages/ra-ui-materialui/src/field/ChipField.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,17 @@ describe('<ChipField />', () => {
);
expect(getByText('foo')).not.toBeNull();
});

it('should render the emptyText when value is null', () => {
const { getByText } = render(
<ChipField
className="className"
classes={{}}
source="name"
record={{ name: null }}
emptyText="NA"
/>
);
expect(getByText('NA')).not.toBeNull();
});
});
26 changes: 24 additions & 2 deletions packages/ra-ui-materialui/src/field/ChipField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import compose from 'recompose/compose';
import get from 'lodash/get';
import pure from 'recompose/pure';
import Chip, { ChipProps } from '@material-ui/core/Chip';
import Typography from '@material-ui/core/Typography';
import { makeStyles } from '@material-ui/core/styles';
import classnames from 'classnames';

Expand All @@ -18,13 +19,34 @@ const useStyles = makeStyles(

export const ChipField: FunctionComponent<
FieldProps & InjectedFieldProps & ChipProps
> = ({ className, classes: classesOverride, source, record = {}, ...rest }) => {
> = ({
className,
classes: classesOverride,
source,
record = {},
emptyText,
...rest
}) => {
const classes = useStyles({ classes: classesOverride });
const value = get(record, source);

if (value == null && emptyText) {
return (
<Typography
component="span"
variant="body2"
className={className}
{...sanitizeRestProps(rest)}
>
{emptyText}
</Typography>
);
}

return (
<Chip
className={classnames(classes.chip, className)}
label={get(record, source)}
label={value}
{...sanitizeRestProps(rest)}
/>
);
Expand Down
73 changes: 40 additions & 33 deletions packages/ra-ui-materialui/src/field/DateField.spec.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
import React from 'react';
import assert from 'assert';
import { shallow } from 'enzyme';
import { render, cleanup } from '@testing-library/react';
import { DateField } from './DateField';

describe('<DateField />', () => {
it('should return null when the record is not set', () =>
assert.equal(shallow(<DateField source="foo" />).html(), null));
afterEach(cleanup);

it('should return null when the record has no value for the source', () =>
assert.equal(
shallow(<DateField record={{}} source="foo" />).html(),
null
));
it('should return null when the record is not set', () => {
const { container } = render(<DateField source="foo" />);
assert.equal(container.firstChild, null);
});

it('should return null when the record has no value for the source', () => {
const { container } = render(<DateField record={{}} source="foo" />);
assert.equal(container.firstChild, null);
});

it('should render a date', () => {
const wrapper = shallow(
const { queryByText } = render(
<DateField
record={{ foo: new Date('2017-04-23') }}
source="foo"
locales="en-US"
/>
);

assert.equal(
wrapper.children().text(),
new Date('2017-04-23').toLocaleDateString('en-US')
);
const date = new Date('2017-04-23').toLocaleDateString('en-US');
assert.notEqual(queryByText(date), null);
});

it('should render a date and time when the showtime prop is passed', () => {
const wrapper = shallow(
const { queryByText } = render(
<DateField
record={{ foo: new Date('2017-04-23 23:05') }}
showTime
Expand All @@ -38,10 +39,8 @@ describe('<DateField />', () => {
/>
);

assert.equal(
wrapper.children().text(),
new Date('2017-04-23 23:05').toLocaleString('en-US')
);
const date = new Date('2017-04-23 23:05').toLocaleString('en-US');
assert.notEqual(queryByText(date), null);
});

it('should pass the options prop to toLocaleString', () => {
Expand All @@ -53,7 +52,7 @@ describe('<DateField />', () => {
day: 'numeric',
};

const wrapper = shallow(
const { queryByText } = render(
<DateField
record={{ foo: date }}
source="foo"
Expand All @@ -62,29 +61,27 @@ describe('<DateField />', () => {
/>
);

return assert.equal(
wrapper.children().text(),
date.toLocaleDateString('en-US', options)
assert.notEqual(
queryByText(date.toLocaleDateString('en-US', options)),
null
);
});

it('should use the locales props as an argument to toLocaleString', () => {
const wrapper = shallow(
const { queryByText } = render(
<DateField
record={{ foo: new Date('2017-04-23') }}
source="foo"
locales="fr-FR"
/>
);

assert.equal(
wrapper.children().text(),
new Date('2017-04-23').toLocaleDateString('fr-FR')
);
const date = new Date('2017-04-23').toLocaleDateString('fr-FR');
assert.notEqual(queryByText(date), null);
});

it('should use custom className', () => {
const wrapper = shallow(
const { container } = render(
<DateField
record={{ foo: new Date('01/01/2016') }}
source="foo"
Expand All @@ -93,21 +90,31 @@ describe('<DateField />', () => {
/>
);

assert.ok(wrapper.is('.foo'));
assert.ok(container.firstChild.classList.contains('foo'));
});

it('should handle deep fields', () => {
const wrapper = shallow(
const { queryByText } = render(
<DateField
record={{ foo: { bar: new Date('01/01/2016') } }}
source="foo.bar"
locales="en-US"
/>
);

assert.equal(
wrapper.children().text(),
new Date('1/1/2016').toLocaleDateString('en-US')
const date = new Date('1/1/2016').toLocaleDateString('en-US');
assert.notEqual(queryByText(date), null);
});

it('should render the emptyText when value is null', () => {
const { queryByText } = render(
<DateField
record={{ foo: null }}
source="foo"
locales="fr-FR"
emptyText="NA"
/>
);
assert.notEqual(queryByText('NA'), null);
});
});
13 changes: 12 additions & 1 deletion packages/ra-ui-materialui/src/field/DateField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export const DateField: FunctionComponent<
Props & InjectedFieldProps & TypographyProps
> = ({
className,
emptyText,
locales,
options,
record,
Expand All @@ -64,8 +65,18 @@ export const DateField: FunctionComponent<
}
const value = get(record, source);
if (value == null) {
return null;
return emptyText ? (
<Typography
component="span"
variant="body2"
className={className}
{...sanitizeRestProps(rest)}
>
{emptyText}
</Typography>
) : null;
}

const date = value instanceof Date ? value : new Date(value);
const dateString = showTime
? toLocaleStringSupportsLocales
Expand Down
51 changes: 33 additions & 18 deletions packages/ra-ui-materialui/src/field/EmailField.spec.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,62 @@
import React from 'react';
import assert from 'assert';
import { shallow } from 'enzyme';
import { render } from '@testing-library/react';
import EmailField from './EmailField';

describe('<EmailField />', () => {
it('should render as an email link', () => {
const record = { foo: 'foo@bar.com' };
const wrapper = shallow(<EmailField record={record} source="foo" />);
const { container } = render(
<EmailField record={record} source="foo" />
);
assert.equal(
wrapper.html(),
container.innerHTML,
'<a href="mailto:foo@bar.com">foo@bar.com</a>'
);
});

it('should handle deep fields', () => {
const record = { foo: { bar: 'foo@bar.com' } };
const wrapper = shallow(
const { container } = render(
<EmailField record={record} source="foo.bar" />
);
assert.equal(
wrapper.html(),
container.innerHTML,
'<a href="mailto:foo@bar.com">foo@bar.com</a>'
);
});

it('should display an email (mailto) link', () => {
const record = { email: 'hal@kubrickcorp.com' };
const wrapper = shallow(<EmailField record={record} source="email" />);
const { container } = render(
<EmailField record={record} source="email" />
);
assert.equal(
wrapper.html(),
container.innerHTML,
'<a href="mailto:hal@kubrickcorp.com">hal@kubrickcorp.com</a>'
);
});

it('should use custom className', () =>
assert.deepEqual(
shallow(
<EmailField
record={{ foo: true }}
source="email"
className="foo"
/>
).prop('className'),
'foo'
));
it('should use custom className', () => {
const { container } = render(
<EmailField
record={{ email: true }}
source="email"
className="foo"
/>
);
assert.ok(container.firstChild.classList.contains('foo'));
});

it('should render the emptyText when value is null', () => {
const { queryByText } = render(
<EmailField record={{ foo: null }} source="foo" emptyText="NA" />
);
assert.notEqual(queryByText('NA'), null);
});

it('should return null when the record has no value for the source', () => {
const { container } = render(<EmailField record={{}} source="foo" />);
assert.equal(container.firstChild, null);
});
});
Loading