Skip to content

Commit

Permalink
Merge pull request #2 from marmelab/reference_many
Browse files Browse the repository at this point in the history
[WIP] Add ReferenceManyField
  • Loading branch information
djhi authored Sep 5, 2016
2 parents 878422e + dd786f5 commit 2a547af
Show file tree
Hide file tree
Showing 31 changed files with 490 additions and 70 deletions.
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
75 changes: 75 additions & 0 deletions src/mui/field/ReferenceManyField.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
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, nameRelatedTo } 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() {
const relatedTo = nameRelatedTo(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, relatedTo);
}

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

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) {
const relatedTo = nameRelatedTo(props.reference, props.record.id, props.resource, props.target);
return {
referenceRecords: getReferences(state, props.reference, relatedTo),
};
}

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

0 comments on commit 2a547af

Please sign in to comment.