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

[WIP] Add ReferenceManyField #2

Merged
merged 14 commits into from
Sep 5, 2016
Merged
Show file tree
Hide file tree
Changes from 13 commits
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
10 changes: 7 additions & 3 deletions docs/Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,14 +146,16 @@ import { List, TextField, EmailField, ReferenceField } from 'admin-on-rest/lib/m
export const PostList = (props) => (
<List {...props}>
<TextField label="id" source="id" />
<ReferenceField label="User" source="userId" reference="users" referenceSource="name" />
<ReferenceField label="User" source="userId" reference="users">
<TextField source="name" />
</ReferenceField>
<TextField label="title" source="title" />
<TextField label="body" source="body" />
</List>
);
```

When displaying the posts list, the browser now fetches related user records, and displays their name.
When displaying the posts list, the browser now fetches related user records, and displays their name as a `<TextField>`.

![reference posts in comment list](http://static.marmelab.com/admin-on-rest/reference_posts.png)

Expand All @@ -169,7 +171,9 @@ import { List, Edit, Create, ReferenceField, TextField, EditButton, DisabledInpu
export const PostList = (props) => (
<List {...props}>
<TextField label="id" source="id" />
<ReferenceField label="User" source="userId" reference="users" referenceSource="name" />
<ReferenceField label="User" source="userId" reference="users">
<TextField source="name" />
</ReferenceField>
<TextField label="title" source="title" />
<TextField label="body" source="body" />
<EditButton />
Expand Down
4 changes: 3 additions & 1 deletion example/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ const CommentFilter = (props) => (
export const CommentList = (props) => (
<List title="All comments" {...props} filter={CommentFilter}>
<TextField label="id" source="id" />
<ReferenceField label="Post" source="post_id" reference="posts" referenceSource="title" />
<ReferenceField label="Post" source="post_id" reference="posts">
<TextField source="title" />
</ReferenceField>
<DateField label="date" source="created_at" />
<EditButton />
</List>
Expand Down
9 changes: 8 additions & 1 deletion example/posts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { List, Filter, Edit, Create, DateField, TextField, EditButton, DisabledInput, TextInput, LongTextInput, DateInput } from 'admin-on-rest/mui';
import { List, Filter, Edit, Create, Datagrid, DateField, TextField, EditButton, DisabledInput, TextInput, LongTextInput, DateInput, ReferenceManyListField } from 'admin-on-rest/mui';

export PostIcon from 'material-ui/svg-icons/action/book';

Expand Down Expand Up @@ -33,6 +33,13 @@ export const PostEdit = (props) => (
<LongTextInput label="Body" source="body" />
<DateInput label="Publication date" source="published_at" />
<TextInput label="Average note" source="average_note" />
<ReferenceManyListField label="Comments" reference="comments" target="post_id">
<Datagrid selectable={false}>
<TextField source="body" />
<DateField source="created_at" />
<EditButton />
</Datagrid>
</ReferenceManyListField>
<DisabledInput label="Nb views" source="views" />
</Edit>
);
Expand Down
12 changes: 12 additions & 0 deletions src/actions/dataActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DELETE,
GET_MANY,
GET_MATCHING,
GET_MANY_REFERENCE,
} from '../rest/types';

export const CRUD_GET_LIST = 'CRUD_GET_LIST';
Expand Down Expand Up @@ -86,3 +87,14 @@ export const crudGetMatching = (reference, relatedTo, filter) => ({
payload: { filter },
meta: { resource: reference, relatedTo, fetch: GET_MATCHING, cancelPrevious: false },
});

export const CRUD_GET_MANY_REFERENCE = 'CRUD_GET_MANY_REFERENCE';
export const CRUD_GET_MANY_REFERENCE_LOADING = 'CRUD_GET_MANY_REFERENCE_LOADING';
export const CRUD_GET_MANY_REFERENCE_FAILURE = 'CRUD_GET_MANY_REFERENCE_FAILURE';
export const CRUD_GET_MANY_REFERENCE_SUCCESS = 'CRUD_GET_MANY_REFERENCE_SUCCESS';

export const crudGetManyReference = (reference, target, id, relatedTo) => ({
type: CRUD_GET_MANY_REFERENCE,
payload: { target, id },
meta: { resource: reference, relatedTo, fetch: GET_MANY_REFERENCE, cancelPrevious: false },
});
27 changes: 20 additions & 7 deletions src/mui/detail/InputList.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
import React from 'react';
import Labeled from '../input/Labeled';

const InputList = ({ record, inputs, resource, handleChange, basePath }) => (
<div>
{record ?
React.Children.map(inputs, input => (
<div key={input.props.source}>
<input.type
{...input.props}
resource={resource}
record={record}
onChange={handleChange}
basePath={basePath}
/>
{input.props.includesLabel ?
React.cloneElement(input, {
resource,
record,
onChange: handleChange,
basePath,

})
:
<Labeled
label={input.props.label}
resource={resource}
record={record}
onChange={handleChange}
basePath={basePath}
>
{input}
</Labeled>
}
</div>
))
:
Expand Down
12 changes: 12 additions & 0 deletions src/mui/field/ChipField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React, { PropTypes } from 'react';
import Chip from 'material-ui/Chip';

const ChipField = ({ source, record = {} }) => <Chip style={{ margin: 4 }}>{record[source]}</Chip>;

ChipField.propTypes = {
source: PropTypes.string.isRequired,
label: PropTypes.string,
record: PropTypes.object,
};

export default ChipField;
26 changes: 22 additions & 4 deletions src/mui/field/ReferenceField.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { Link } from 'react-router';
import LinearProgress from 'material-ui/LinearProgress';
import { crudGetOneReference as crudGetOneReferenceAction } from '../../actions/referenceActions';

/**
* @example
* <ReferenceField label="Post" source="post_id" reference="posts">
* <TextField source="title" />
* </ReferenceField>
*/
export class ReferenceField extends Component {
componentDidMount() {
this.props.crudGetOneReference(this.props.reference, this.props.record[this.props.source]);
Expand All @@ -16,12 +22,24 @@ export class ReferenceField extends Component {
}

render() {
const { record, source, reference, referenceRecord, referenceSource, basePath, allowEmpty } = this.props;
const rootPath = basePath.split('/').slice(0, -1).join('/');
const { record, source, reference, referenceRecord, basePath, allowEmpty, children } = this.props;
if (React.Children.count(children) !== 1) {
throw new Error('<ReferenceField> only accepts a single child');
}
if (!referenceRecord && !allowEmpty) {
return <LinearProgress />;
}
return <Link to={`${rootPath}/${reference}/${record[source]}`}>{referenceRecord[referenceSource]}</Link>;
const rootPath = basePath.split('/').slice(0, -1).join('/');
return (
<Link to={`${rootPath}/${reference}/${record[source]}`}>
{React.cloneElement(children, {
record: referenceRecord,
resource: reference,
allowEmpty,
basePath,
})}
</Link>
);
}
}

Expand All @@ -31,9 +49,9 @@ ReferenceField.propTypes = {
record: PropTypes.object,
allowEmpty: PropTypes.bool.isRequired,
reference: PropTypes.string.isRequired,
referenceSource: PropTypes.string.isRequired,
referenceRecord: PropTypes.object,
basePath: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
crudGetOneReference: PropTypes.func.isRequired,
};

Expand Down
72 changes: 72 additions & 0 deletions src/mui/field/ReferenceManyField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import LinearProgress from 'material-ui/LinearProgress';
import { crudGetManyReference as crudGetManyReferenceAction } from '../../actions/dataActions';
import { getReferences, relatedTo } from '../../reducer/references/oneToMany';

/**
* Render related records in a list of a single field.
*
* The child field will be repeated as many times as there are related records.
*
* @example Display all the books by the current author
* <ReferenceManyField reference="books" target="author_id">
* <ChipField source="title" />
* </ReferenceManyField>
*/
export class ReferenceManyField extends Component {
componentDidMount() {
this.props.crudGetManyReference(this.props.reference, this.props.target, this.props.record.id, relatedTo(this.props.reference, this.props.record.id, this.props.resource, this.props.target));
Copy link
Contributor

@djhi djhi Sep 5, 2016

Choose a reason for hiding this comment

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

I know you like one liners but this is annoying to review on github.

const relatedField = relatedTo(this.props.reference, this.props.record.id, this.props.resource, this.props.target);
this.props.crudGetManyReference(this.props.reference, this.props.target, this.props.record.id, relatedField);

same for the others in componentWillReceiveProps, mapStateToProps and ReferenceManyListField ?

}

componentWillReceiveProps(nextProps) {
if (this.props.record.id !== nextProps.record.id) {
this.props.crudGetManyReference(nextProps.reference, nextProps.target, nextProps.record.id, relatedTo(nextProps.reference, nextProps.record.id, nextProps.resource, nextProps.target));
}
}

render() {
const { resource, reference, referenceRecords, children, basePath } = this.props;
if (React.Children.count(children) !== 1) {
throw new Error('<ReferenceManyField> only accepts a single child');
}
if (typeof referenceRecords === 'undefined') {
return <LinearProgress style={{ marginTop: '1em' }} />;
}
const referenceBasePath = basePath.replace(resource, reference); // FIXME obviously very weak
return (
<div style={{ display: 'flex', flexWrap: 'wrap' }}>
{Object.keys(referenceRecords).map(index =>
React.cloneElement(children, {
key: index,
record: referenceRecords[index],
resource: reference,
basePath: referenceBasePath,
})
)}
</div>
);
}
}

ReferenceManyField.propTypes = {
resource: PropTypes.string.isRequired,
record: PropTypes.object,
label: PropTypes.string,
reference: PropTypes.string.isRequired,
target: PropTypes.string.isRequired,
referenceRecords: PropTypes.object,
basePath: PropTypes.string.isRequired,
children: PropTypes.element.isRequired,
crudGetManyReference: PropTypes.func.isRequired,
};

function mapStateToProps(state, props) {
return {
referenceRecords: getReferences(state, props.reference, relatedTo(props.reference, props.record.id, props.resource, props.target)),
};
}

export default connect(mapStateToProps, {
crudGetManyReference: crudGetManyReferenceAction,
})(ReferenceManyField);
71 changes: 71 additions & 0 deletions src/mui/field/ReferenceManyField.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from 'react';
import assert from 'assert';
import { shallow } from 'enzyme';
import { ReferenceManyField } from './ReferenceManyField';
import TextField from './TextField';

describe('<ReferenceManyField />', () => {
it('should render a loading indicator when related records are not yet fetched', () => {
const wrapper = shallow(
<ReferenceManyField
resource="foo"
reference="bar"
target="foo_id"
basePath=""
crudGetManyReference={() => {}}
>
<TextField source="title" />
</ReferenceManyField>
);
const ProgressElements = wrapper.find('LinearProgress');
assert.equal(ProgressElements.length, 1);
const TextFieldElements = wrapper.find('TextField');
assert.equal(TextFieldElements.length, 0);
});

it('should render a list of the child component', () => {
const referenceRecords = {
1: { id: 1, title: 'hello' },
2: { id: 2, title: 'world' },
};
const wrapper = shallow(
<ReferenceManyField
resource="foo"
reference="bar"
target="foo_id"
basePath=""
referenceRecords={referenceRecords}
crudGetManyReference={() => {}}
>
<TextField source="title" />
</ReferenceManyField>
);
const ProgressElements = wrapper.find('LinearProgress');
assert.equal(ProgressElements.length, 0);
const TextFieldElements = wrapper.find('TextField');
assert.equal(TextFieldElements.length, 2);
assert.equal(TextFieldElements.at(0).prop('resource'), 'bar');
assert.deepEqual(TextFieldElements.at(0).prop('record'), { id: 1, title: 'hello' });
assert.equal(TextFieldElements.at(1).prop('resource'), 'bar');
assert.deepEqual(TextFieldElements.at(1).prop('record'), { id: 2, title: 'world' });
});

it('should render nothing when there are no related records', () => {
const wrapper = shallow(
<ReferenceManyField
resource="foo"
reference="bar"
target="foo_id"
basePath=""
referenceRecords={{}}
crudGetManyReference={() => {}}
>
<TextField source="title" />
</ReferenceManyField>
);
const ProgressElements = wrapper.find('LinearProgress');
assert.equal(ProgressElements.length, 0);
const TextFieldElements = wrapper.find('TextField');
assert.equal(TextFieldElements.length, 0);
});
});
Loading