From 315460c26ac6b762cd71afcfa6149274b7c575d1 Mon Sep 17 00:00:00 2001 From: Kirk Date: Thu, 19 Jul 2018 23:14:59 +0800 Subject: [PATCH 01/20] New translations actions.md (Chinese Simplified) --- website/translated_docs/zh-CN/actions.md | 506 +++++++++++++++++++++++ 1 file changed, 506 insertions(+) create mode 100644 website/translated_docs/zh-CN/actions.md diff --git a/website/translated_docs/zh-CN/actions.md b/website/translated_docs/zh-CN/actions.md new file mode 100644 index 0000000..a4a386d --- /dev/null +++ b/website/translated_docs/zh-CN/actions.md @@ -0,0 +1,506 @@ +--- +id: actions +title: Writing Actions +--- +Admin interfaces often have to offer custom actions, beyond the simple CRUD. For instance, in an administration for comments, an "Approve" button (allowing to update the `is_approved` property and to save the updated record in one click) - is a must have. + +How can you add such custom actions with react-admin? The answer is twofold, and learning to do it properly will give you a better understanding of how react-admin uses Redux and redux-saga. + +## The Simple Way + +Here is an implementation of the "Approve" button that works perfectly: + +```jsx +// in src/comments/ApproveButton.js +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import FlatButton from '@material-ui/core/FlatButton'; +import { showNotification as showNotificationAction } from 'react-admin'; +import { push as pushAction } from 'react-router-redux'; + +class ApproveButton extends Component { + handleClick = () => { + const { push, record, showNotification } = this.props; + const updatedRecord = { ...record, is_approved: true }; + fetch(`/comments/${record.id}`, { method: 'PUT', body: updatedRecord }) + .then(() => { + showNotification('Comment approved'); + push('/comments'); + }) + .catch((e) => { + console.error(e); + showNotification('Error: comment not approved', 'warning') + }); + } + + render() { + return ; + } +} + +ApproveButton.propTypes = { + push: PropTypes.func, + record: PropTypes.object, + showNotification: PropTypes.func, +}; + +export default connect(null, { + showNotification: showNotificationAction, + push: pushAction, +})(ApproveButton); +``` + +The `handleClick` function makes a `PUT` request the REST API with `fetch`, then displays a notification (with `showNotification`) and redirects to the comments list page (with `push`); + +`showNotification` and `push` are *action creators*. This is a Redux term for functions that return a simple action object. When given an object of action creators in the second argument, `connect()` will [decorate each action creator](https://github.com/reactjs/react-redux/blob/master/docs/api.md#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options) with Redux' `dispatch` method, so in the `handleClick` function, a call to `showNotification()` is actually a call to `dispatch(showNotification())`. + +This `ApproveButton` can be used right away, for instance in the list of comments, where `` automatically injects the `record` to its children: + +```jsx +// in src/comments/index.js +import ApproveButton from './ApproveButton'; + +export const CommentList = (props) => + + + + + + + + + ; +``` + +Or, in the `` page, as a [custom action](./CreateEdit.md#actions): + +```jsx +// in src/comments/CommentEditActions.js +import React from 'react'; +import CardActions from '@material-ui/core/CardActions'; +import { ListButton, DeleteButton } from 'react-admin'; +import ApproveButton from './ApproveButton'; + +const cardActionStyle = { + zIndex: 2, + display: 'inline-block', + float: 'right', +}; + +const CommentEditActions = ({ basePath, data, resource }) => ( + + + + + +); + +export default CommentEditActions; + +// in src/comments/index.js +import CommentEditActions from './CommentEditActions'; + +export const CommentEdit = (props) => + }> + ... + ; +``` + +## Using a Data Provider Instead of Fetch + +The previous code uses `fetch()`, which means it has to make raw HTTP requests. The REST logic often requires a bit of HTTP plumbing to deal with query parameters, encoding, headers, body formatting, etc. It turns out you probably already have a function that maps from a REST request to an HTTP request: the [Data Provider](./DataProviders.md). So it's a good idea to use this function instead of `fetch` - provided you have exported it: + +```jsx +// in src/dataProvider.js +import jsonServerProvider from 'ra-data-json-server'; +export default jsonServerProvider('http://Mydomain.com/api/'); + +// in src/comments/ApproveButton.js +import { UPDATE } from 'react-admin'; +import dataProvider from '../dataProvider'; + +class ApproveButton extends Component { + handleClick = () => { + const { push, record, showNotification } = this.props; + const updatedRecord = { ...record, is_approved: true }; + dataProvider(UPDATE, 'comments', { id: record.id, data: updatedRecord }) + .then(() => { + showNotification('Comment approved'); + push('/comments'); + }) + .catch((e) => { + console.error(e); + showNotification('Error: comment not approved', 'warning') + }); + } + + render() { + return ; + } +} +``` + +There you go: no more `fetch`. Just like `fetch`, the `dataProvider` returns a `Promise`. It's signature is: + +```jsx +/** + * Query a data provider and return a promise for a response + * + * @example + * dataProvider(GET_ONE, 'posts', { id: 123 }) + * => new Promise(resolve => resolve({ id: 123, title: "hello, world" })) + * + * @param {string} type Request type, e.g GET_LIST + * @param {string} resource Resource name, e.g. "posts" + * @param {Object} payload Request parameters. Depends on the action type + * @returns {Promise} the Promise for a response + */ +const dataProvider = (type, resource, params) => new Promise(); +``` + +As for the syntax of the various request types (`GET_LIST`, `GET_ONE`, `UPDATE`, etc.), head to the [Data Provider documentation](./DataProviders.md#request-format) for more details. + +## Using a Custom Action Creator + +Fetching data right inside the component is easy. But if you're a Redux user, you might want to do it in a more idiomatic way - by dispatching actions. First, create your own action creator to replace the call to `dataProvider`: + +```jsx +// in src/comment/commentActions.js +import { UPDATE } from 'react-admin'; +export const COMMENT_APPROVE = 'COMMENT_APPROVE'; +export const commentApprove = (id, data, basePath) => ({ + type: COMMENT_APPROVE, + payload: { id, data: { ...data, is_approved: true } }, + meta: { resource: 'comments', fetch: UPDATE }, +}); +``` + +This action creator takes advantage of react-admin's built in fetcher, which listens to actions with the `fetch` meta. Upon dispatch, this action will trigger the call to `dataProvider(UPDATE, 'comments')`, dispatch a `COMMENT_APPROVE_LOADING` action, then after receiving the response, dispatch either a `COMMENT_APPROVE_SUCCESS`, or a `COMMENT_APPROVE_FAILURE`. + +To use the new action creator in the component, `connect` it: + +```jsx +// in src/comments/ApproveButton.js +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import Button from '@material-ui/core/Button'; +import { commentApprove as commentApproveAction } from './commentActions'; + +class ApproveButton extends Component { + handleClick = () => { + const { commentApprove, record } = this.props; + commentApprove(record.id, record); + // how about push and showNotification? + } + + render() { + return ; + } +} + +ApproveButton.propTypes = { + commentApprove: PropTypes.func, + record: PropTypes.object, +}; + +export default connect(null, { + commentApprove: commentApproveAction, +})(ApproveButton); +``` + +This works fine: when a user presses the "Approve" button, the API receives the `UPDATE` call, and that approves the comment. But it's not possible to call `push` or `showNotification` in `handleClick` anymore. This is because `commentApprove()` returns immediately, whether the API call succeeds or not. How can you run a function only when the action succeeds? + +## Handling Side Effects + +Fetching data is called a *side effect*, since it calls the outside world, and is asynchronous. Usual actions may have other side effects, like showing a notification, or redirecting the user to another page. Just like for the `fetch` side effect, you can associate side effects to an action declaratively by setting the appropriate keys in the action `meta`. + +For instance, to display a notification when the `COMMENT_APPROVE` action is dispatched, add the `notification` meta: + +```diff +// in src/comment/commentActions.js +import { UPDATE } from 'react-admin'; +export const COMMENT_APPROVE = 'COMMENT_APPROVE'; +export const commentApprove = (id, data, basePath) => ({ + type: COMMENT_APPROVE, + payload: { id, data: { ...data, is_approved: true } }, + meta: { + resource: 'comments', + fetch: UPDATE, ++ notification: { ++ body: 'resources.comments.notification.approved_success', ++ level: 'info', ++ }, ++ redirectTo: '/comments', ++ basePath, + }, +}); +``` + +React-admin can handle the following side effects metas: + +* `notification`: Display a notification. The property value should be an object describing the notification to display. The `body` can be a translation key. `level` can be either `info` or `warning`. +* `redirectTo`: Redirect the user to another page. The property value should be the path to redirect the user to. +* `refresh`: Force a rerender of the current view (equivalent to pressing the Refresh button). Set to true to enable. +* `unselectAll`: Unselect all lines in the current datagrid. Set to true to enable. +* `basePath`: This is not a side effect, but it's used internaly to compute redirection paths. Set it when you have a redirection side effect. + +## Success and Failure Side Effects + +In the previous example, the "notification approved" notification appears when the `COMMENT_APPROVE` action is dispatched, i.e. *before* the server is even called. That's a bit too early: what if the server returns an error? + +In practice, most side effects must be triggered after the `fetch` side effect succeeds or fails. To support that, you can enclose side effects under the `onSuccess` and `onFailure` keys in the `meta` property of an action: + +```diff +// in src/comment/commentActions.js +import { UPDATE } from 'react-admin'; +export const COMMENT_APPROVE = 'COMMENT_APPROVE'; +export const commentApprove = (id, data, basePath) => ({ + type: COMMENT_APPROVE, + payload: { id, data: { ...data, is_approved: true } }, + meta: { + resource: 'comments', + fetch: UPDATE, +- notification: { +- body: 'resources.comments.notification.approved_success', +- level: 'info', +- }, +- redirectTo: '/comments', +- basePath, ++ onSuccess: { ++ notification: { ++ body: 'resources.comments.notification.approved_success', ++ level: 'info', ++ }, ++ redirectTo: '/comments', ++ basePath, ++ }, ++ onFailure: { ++ notification: { ++ body: 'resources.comments.notification.approved_failure', ++ level: 'warning', ++ }, ++ }, + }, +}); +``` + +In this case, no side effect is triggered when the `COMMENT_APPROVE` action is dispatched. However, when the `fetch` side effects returns successfully, react-admin dispatches a `COMMENT_APPROVE_SUCCESS` action, and copies the `onSuccess` side effects into the `meta` property. So it will dispatch an action looking like: + +```js +{ + type: COMMENT_APPROVE_SUCCESS, + payload: { data: { /* data returned by the server */ } }, + meta: { + resource: 'comments', + notification: { + body: 'resources.comments.notification.approved_success', + level: 'info', + }, + redirectTo: '/comments', + basePath, + }, +} +``` + +And then, the side effects will trigger. With this code, approving a review now displays the correct notification, and redirects to the comment list. + +You can use `onSuccess` and `onFailure` metas in your own actions to handle side effects. + +## Custom sagas + +Sometimes, you may want to trigger other *side effects*. React-admin promotes a programming style where side effects are decoupled from the rest of the code, which has the benefit of making them testable. + +In react-admin, side effects are handled by Sagas. [Redux-saga](https://redux-saga.github.io/redux-saga/) is a side effect library built for Redux, where side effects are defined by generator functions. If this is new to you, take a few minutes to go through the Saga documentation. + +Here is the generator function necessary to handle the side effects for a failed `COMMENT_APPROVE` action which would log the error with an external service such as [Sentry](https://sentry.io): + +```jsx +// in src/comments/commentSaga.js +import { call, takeEvery } from 'redux-saga/effects'; + +function* commentApproveFailure({ error }) { + yield call(Raven.captureException, error); +} + +export default function* commentSaga() { + yield takeEvery('COMMENT_APPROVE_FAILURE', commentApproveFailure); +} +``` + +Let's explain all of that, starting with the final `commentSaga` generator function. A [generator function](http://exploringjs.com/es6/ch_generators.html) (denoted by the `*` in the function name) gets paused on statements called by `yield` - until the yielded statement returns. `yield takeEvery([ACTION_NAME], callback)` executes the provided callback [every time the related action is called](https://redux-saga.github.io/redux-saga/docs/basics/UsingSagaHelpers.html). To summarize, this will execute `commentApproveFailure` when the fetch initiated by `commentApprove()` fails. + +As for `commentApproveFailure`, it just dispatch a [`call`](https://redux-saga.js.org/docs/api/#callfn-args) side effect to the `captureException` function from the global `Raven` object. + +To use this saga, pass it in the `customSagas` props of the `` component: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin, Resource } from 'react-admin'; + +import { CommentList } from './comments'; +import commentSaga from './comments/commentSaga'; + +const App = () => ( + + + +); + +export default App; +``` + +With this code, a failed review approval now sends the the correct signal to Sentry. + +**Tip**: The side effects are [testable](https://redux-saga.github.io/redux-saga/docs/introduction/BeginnerTutorial.html#making-our-code-testable), too. + +## Optimistic Rendering and Undo + +In the previous example, after clicking on the "Approve" button, a spinner displays while the data provider is fetched. Then, users are redirected to the comments list. But in most cases, the server returns a success response, so the user waits for this response for nothing. + +For its own fetch actions, react-admin uses an approach called *optimistic rendering*. The idea is to handle the `fetch` actions on the client side first (i.e. updating entities in the Redux store), and re-render the screen immediately. The user sees the effect of their action with no delay. Then, react-admin applies the success side effects, and only after that it triggers the fetch to the data provider. If the fetch ends with a success, react-admin does nothing more than a refresh to grab the latest data from the server, but in most cases, the user sees no difference (the data in the Redux store and the data from the data provider are the same). If the fetch fails, react-admin shows an error notification, and forces a refresh, too. + +As a bonus, while the success notification is displayed, users have the ability to cancel the action *before* the data provider is even called. + +To make an action with a `fetch` meta optimistic, decorate it with the `startUndoable` action creator: + +```diff +// in src/comments/ApproveButton.js +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import Button from '@material-ui/core/Button'; ++ import { startUndoable as startUndoableAction } from 'ra-core'; +- import { commentApprove as commentApproveAction } from './commentActions'; ++ import { commentApprove } from './commentActions'; + +class ApproveButton extends Component { + handleClick = () => { +- const { commentApprove, record } = this.props; +- commentApprove(record.id, record); ++ const { startUndoable, record } = this.props; ++ startUndoable(commentApprove(record.id, record)); + } + + render() { + return ; + } +} + +ApproveButton.propTypes = { +- commentApprove: PropTypes.func, ++ startUndoable: PropTypes.func, + record: PropTypes.object, +}; + +export default connect(null, { +- commentApprove: commentApproveAction, ++ startUndoable: startUndoableAction, +})(ApproveButton); +``` + +And that's all it takes to make a fetch action optimistic. Note that the `startUndoable` action creator is passed to Redux `connect` as `mapDispatchToProp`, to be decorated with `dispatch` - but `commentApprove` is not. Only the first action must be decorated with dispatch. + +The fact that react-admin updates the internal store if you use custom actions with the `fetch` meta should be another motivation to avoid using raw `fetch`. + +## Using a Custom Reducer + +In addition to triggering REST calls, you may want to store the effect of your own actions in the application state. For instance, if you want to display a widget showing the current exchange rate for the bitcoin, you might need the following action: + +```jsx +// in src/bitcoinRateReceived.js +export const BITCOIN_RATE_RECEIVED = 'BITCOIN_RATE_RECEIVED'; +export const bitcoinRateReceived = (rate) => ({ + type: BITCOIN_RATE_RECEIVED, + payload: { rate }, +}); +``` + +This action can be triggered on mount by the following component: + +```jsx +// in src/BitCoinRate.js +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { bitcoinRateReceived as bitcoinRateReceivedAction } from './bitcoinRateReceived'; + +class BitCoinRate extends Component { + componentWillMount() { + fetch('https://blockchain.info/fr/ticker') + .then(response => response.json()) + .then(rates => rates.USD['15m']) + .then(bitcoinRateReceived) // dispatch action when the response is received + } + + render() { + const { rate } = this.props; + return
Current bitcoin value: {rate}$
+ } +} + +BitCoinRate.propTypes = { + bitcoinRateReceived: PropTypes.func, + rate: PropTypes.number, +}; + +const mapStateToProps = state => ({ rate: state.bitcoinRate }); + +export default connect(mapStateToProps, { + bitcoinRateReceived: bitcoinRateReceivedAction, +})(BitCoinRate); +``` + +In order to put the rate passed to `bitcoinRateReceived()` into the Redux store, you'll need a reducer: + +```jsx +// in src/rateReducer.js +import { BITCOIN_RATE_RECEIVED } from './bitcoinRateReceived'; + +export default (previousState = 0, { type, payload }) => { + if (type === BITCOIN_RATE_RECEIVED) { + return payload.rate; + } + return previousState; +} +``` + +Now the question is: How can you put this reducer in the `` app? Simple: use the `customReducers` props: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin } from 'react-admin'; + +import rate from './rateReducer'; + +const App = () => ( + + ... + +); + +export default App; +``` + +**Tip**: You can avoid storing data in the Redux state by storing data in a component state instead. It's much less complicated to deal with, and more performant, too. Use the global state only when you really need to. + +## List Bulk Actions + +Almost everything we saw before is true for custom `List` bulk actions too, with the following few differences: + +* They receive the following props: `resource`, `selectedIds` and `filterValues` +* They do not receive the current record in the `record` prop as there are many of them. +* They must render as a material-ui [`MenuItem`](http://www.material-ui.com/#/components/menu). + +You can find a complete example in the `List` documentation, in the [`bulk-actions`](/List.html#bulk-actions) section. + +## Conclusion + +Which style should you choose for your own action buttons? + +The first version (with `fetch`) is perfectly fine, and if you're not into unit testing your components, or decoupling side effects from pure functions, then you can stick with it without problem. + +On the other hand, if you want to promote reusability, separation of concerns, adhere to react-admin's coding standards, and if you know enough Redux and Saga, use the final version. \ No newline at end of file From e8cb50270a56ff35bca08c3ba3e85fe36911c926 Mon Sep 17 00:00:00 2001 From: Kirk Date: Thu, 19 Jul 2018 23:15:01 +0800 Subject: [PATCH 02/20] New translations intro.md (Chinese Simplified) --- website/translated_docs/zh-CN/intro.md | 184 +++++++++++++++++++++++++ 1 file changed, 184 insertions(+) create mode 100644 website/translated_docs/zh-CN/intro.md diff --git a/website/translated_docs/zh-CN/intro.md b/website/translated_docs/zh-CN/intro.md new file mode 100644 index 0000000..1b445b3 --- /dev/null +++ b/website/translated_docs/zh-CN/intro.md @@ -0,0 +1,184 @@ +--- +id: intro +title: Introduction +--- +A frontend Framework for building admin applications running in the browser on top of REST/GraphQL APIs, using ES6, [React](https://facebook.github.io/react/) and [Material Design](https://material.io/). Previously named [admin-on-rest](https://github.com/marmelab/admin-on-rest). Open sourced and maintained by [marmelab](https://marmelab.com/). + +[Demo](https://marmelab.com/react-admin-demo/) - [Source](https://github.com/marmelab/react-admin) - [Releases](https://github.com/marmelab/react-admin/releases) - [Support](http://stackoverflow.com/questions/tagged/react-admin) + +## Features + +* Adapts to any backend (REST, GraphQL, SOAP, etc.) +* Complete documentation +* Super-fast UI thanks to optimistic rendering (renders before the server returns) +* Undo updates and deletes for a few seconds +* Supports relationships (many to one, one to many) +* Internationalization (i18n) +* Conditional formatting +* Themeable +* Supports any authentication provider (REST API, OAuth, Basic Auth, ...) +* Full-featured Datagrid (sort, pagination, filters) +* Filter-as-you-type +* Supports any form layout (simple, tabbed, etc.) +* Data Validation +* Custom actions +* Large library of components for various data types: boolean, number, rich text, etc. +* WYSIWYG editor +* Customize dashboard, menu, layout +* Super easy to extend and override (it's just React components) +* Highly customizable interface +* Can connect to multiple backends +* Leverages the best libraries in the React ecosystem (Redux, redux-form, redux-saga, material-ui, recompose) +* Can be included in another React app +* Inspired by the popular [ng-admin](https://github.com/marmelab/ng-admin) library (also by marmelab) + +## Installation + +React-admin is available from npm. You can install it (and its required dependencies) using: + +```sh +npm install react-admin +``` + +## Usage + +Read the [Tutorial](./Tutorial.md) for a 15 minutes introduction. After that, head to the [Documentation](./index.md), or checkout the [source code of the demo](https://github.com/marmelab/react-admin/tree/master/examples/demo) for an example usage. + +## At a Glance + +```jsx +// in app.js +import React from 'react'; +import { render } from 'react-dom'; +import { Admin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; + +import { PostList, PostEdit, PostCreate, PostIcon } from './posts'; + +render( + + + , + document.getElementById('root') +); +``` + +The `` component is a configuration component that allows to define sub components for each of the admin view: `list`, `edit`, and `create`. These components use Material UI and custom components from react-admin: + +```jsx +// in posts.js +import React from 'react'; +import { List, Datagrid, Edit, Create, SimpleForm, DateField, TextField, EditButton, DisabledInput, TextInput, LongTextInput, DateInput } from 'react-admin'; +import BookIcon from '@material-ui/icons/Book'; +export const PostIcon = BookIcon; + +export const PostList = (props) => ( + + + + + + + + + + +); + +const PostTitle = ({ record }) => { + return Post {record ? `"${record.title}"` : ''}; +}; + +export const PostEdit = (props) => ( + } {...props}> + + + + + + + + + + +); + +export const PostCreate = (props) => ( + + + + + + + + + +); +``` + +## Does It Work With My API? + +Yes. + +React-admin uses an adapter approach, with a concept called *Data Providers*. Existing providers can be used as a blueprint to design your API, or you can write your own Data Provider to query an existing API. Writing a custom Data Provider is a matter of hours. + +![Data Provider architecture](https://marmelab.com/react-admin/img/data-provider.png) + +See the [Data Providers documentation](./DataProviders.md) for details. + +## Batteries Included But Removable + +React-admin is designed as a library of loosely coupled React components built on top of [material-ui](http://www.material-ui.com/#/), in addition to controller functions implemented the Redux way. It is very easy to replace one part of react-admin with your own, e.g. to use a custom datagrid, GraphQL instead of REST, or bootstrap instead of Material Design. + +## Contributing + +Pull requests are welcome on the [GitHub repository](https://github.com/marmelab/react-admin). Try to follow the coding style of the existing files, and include unit tests and documentation. Be prepared for a thorough code review, and be patient for the merge - this is an open-source initiative. + +You can run the example app by calling: + +```sh +make run +``` + +And then browse to . + +If you want to contribute to the documentation, install jekyll, then call + +```sh +make doc +``` + +And then browse to + +You can run the unit tests by calling + +```sh +make test +``` + +If you are using react-admin as a dependency, and if you want to try and hack it, here is the advised process: + +```sh +# in myapp +# install react-admin from GitHub in another directory +$ cd .. +$ git clone git@github.com:marmelab/react-admin.git && cd react-admin && make install +# replace your node_modules/react-admin by a symbolic link to the github checkout +$ cd ../myapp +$ npm link ../react-admin +# go back to the checkout, and replace the version of react by the one in your app +$ cd ../react-admin +$ npm link ../myapp/node_modules/react +$ make watch +# in another terminal, go back to your app, and start it as usual +$ cd ../myapp +$ npm run +``` + +## License + +React-admin is licensed under the [MIT Licence](https://github.com/marmelab/react-admin/blob/master/LICENSE.md), sponsored and supported by [marmelab](http://marmelab.com). + +## Donate + +This library is free to use, even for commercial purpose. If you want to give back, please talk about it, help newcomers, or contribute code. But the best way to give back is to **donate to a charity**. We recommend [Doctors Without Borders](http://www.doctorswithoutborders.org/). \ No newline at end of file From 229358ac977258ef4804b0350f009053c2a5c01c Mon Sep 17 00:00:00 2001 From: Kirk Date: Thu, 19 Jul 2018 23:15:02 +0800 Subject: [PATCH 03/20] New translations tutorial.md (Chinese Simplified) --- website/translated_docs/zh-CN/tutorial.md | 688 ++++++++++++++++++++++ 1 file changed, 688 insertions(+) create mode 100644 website/translated_docs/zh-CN/tutorial.md diff --git a/website/translated_docs/zh-CN/tutorial.md b/website/translated_docs/zh-CN/tutorial.md new file mode 100644 index 0000000..2c829f5 --- /dev/null +++ b/website/translated_docs/zh-CN/tutorial.md @@ -0,0 +1,688 @@ +--- +id: tutorial +title: Tutorial +--- +This 15 minutes tutorial will expose how to create a new admin app based on an existing REST API. + +## Setting Up + +React-admin uses React. We'll use Facebook's [create-react-app](https://github.com/facebookincubator/create-react-app) to create an empty React app, and install the `react-admin` package: + +```sh +npm install -g create-react-app +create-react-app test-admin +cd test-admin/ +yarn add react-admin +yarn start +``` + +You should be up and running with an empty React application on port 3000. + +## Using an API As Data Source + +React-admin runs in the browser, and uses APIs for fetching and storing data. + +We'll be using [JSONPlaceholder](http://jsonplaceholder.typicode.com/), a fake REST API designed for testing and prototyping, as the datasource for the admin. Here is what it looks like: + + curl http://jsonplaceholder.typicode.com/posts/12 + + +```json +{ + "id": 12, + "title": "in quibusdam tempore odit est dolorem", + "body": "itaque id aut magnam\npraesentium quia et ea odit et ea voluptas et\nsapiente quia nihil amet occaecati quia id voluptatem\nincidunt ea est distinctio odio", + "userId": 2 +} +``` + +JSONPlaceholder provides endpoints for posts, comments, and users. The admin we'll build should allow to Create, Retrieve, Update, and Delete (CRUD) these resources. + +## Making Contact With The API Using a Data Provider + +Bootstrap the admin app by replacing the `src/App.js` by the following code: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin, Resource } from 'react-admin'; +import jsonServerProvider from 'ra-data-json-server'; + +const dataProvider = jsonServerProvider('http://jsonplaceholder.typicode.com'); +const App = () => ; + +export default App; +``` + +The `App` component now renders an `` component, which is the root component of a react-admin application. This component expects a `dataProvider` prop - a function capable of fetching data from an API. Since there is no standard for data exchanges between computers, you will probably have to write a custom provider to connect react-admin to your own APIs - but we'll dive into Data Providers later. For now, let's take advantage of the `ra-data-json-server` data provider, which speaks the same REST dialect as JSONPlaceholder. + +```sh +yarn add ra-data-json-server +``` + +That's enough for react-admin to run an empty app. Now it's time to add features! + +## Mapping API Endpoints With Resources + +The `` component expects one or more `` child components. Each resource maps a name to an endpoint in the API. Edit the `App.js` file to add a `posts` resource: + +```jsx +// in src/App.js +import { PostList } from './posts'; + +const App = () => ( + + + +); +``` + +**Tip**: We'll define the `` component in the next section. + +The line `` informs react-admin to fetch the "posts" records from the URL. + +## Displaying A List Of Records + +`` also defines the React components to use for each CRUD operation (`list`, `create`, `edit`, and `show`). The `list={PostList}` prop means that react-admin should use the `` component to display the list of posts. Create that component as follows: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Datagrid, TextField } from 'react-admin'; + +export const PostList = (props) => ( + + + + + + + +); +``` + +The main component of the post list is a `` component, responsible for grabbing the information from the API, displaying the page title, and handling pagination. This list then delegates the display of the actual list of posts to its child. In this case, that's a `` component, which renders a table with one row for each record. The datagrid uses its child components (here, a list of ``) to determine the columns to render. Each Field component maps a different field in the API response, specified by the `source` prop. + +That's enough to display the post list: + +![Simple posts list](https://marmelab.com/react-admin/img/simple-post-list.png) + +If you look at your browser network tab in the developer tools, you'll notice that the application fetched the `http://jsonplaceholder.typicode.com/posts` URL, then used the results to build the datagrid. That's basically how react-admin works. + +The list is already functional: you can reorder it by clicking on column headers, or change pages by using the bottom pagination controls. The `ra-data-json-server` data provider translates these actions to a query string that JSONPlaceholder understands. + +## Using Field Types + +You've just met the `` component. React-admin provides more Field components, mapping various data types: number, date, image, HTML, array, reference, etc. + +For instance, [the `/users` endpoint in JSONPlaceholder](http://jsonplaceholder.typicode.com/users) contains emails. + + curl http://jsonplaceholder.typicode.com/users/2 + + +```json +{ + "id": 2, + "name": "Ervin Howell", + "username": "Antonette", + "email": "Shanna@melissa.tv", +} +``` + +Let's create a new `users` resource to fetch that endpoint. Add it in `src/App.js`: + +```jsx +// in src/App.js +import { PostList } from './posts'; +import { UserList } from './users'; + +const App = () => ( + + + + +); +``` + +Now, create a `users.js` file exporting a `UserList`, using `` to map the `email` field: + +```jsx +// in src/users.js +import React from 'react'; +import { List, Datagrid, EmailField, TextField } from 'react-admin'; + +export const UserList = (props) => ( + + + + + + + + +); +``` + +![Simple user datagrid](https://marmelab.com/react-admin/img/simple-user-list.png) + +The sidebar now gives access to the second resource, "users". You can click on it, it's working! The users list shows email addresses as a `` tags. + +In react-admin, fields are simple React components. At runtime, they receive the `record` fetched from the API (e.g. `{ "id": 2, "name": "Ervin Howell", "username": "Antonette", "email": "Shanna@melissa.tv" }`), and the `source` field they should display (e.g. `email`). + +That means that writing a custom Field component is really simple. For instance, to create an `UrlField`: + +```jsx +// in src/MyUrlField.js +import React from 'react'; +import PropTypes from 'prop-types'; + +const UrlField = ({ record = {}, source }) => + + {record[source]} + ; + +UrlField.propTypes = { + record: PropTypes.object, + source: PropTypes.string.isRequired, +}; + +export default UrlField; +``` + +## Handling Relationships + +In JSONPlaceholder, each `post` record includes a `userId` field, which points to a `user`: + +```json +{ + "id": 1, + "title": "sunt aut facere repellat provident occaecati excepturi optio reprehenderit", + "body": "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto", + "userId": 1 +} +``` + +React-admin knows how to take advantage of these foreign keys to fetch references. For instance, to include the user name in the posts list, use the ``: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Datagrid, TextField, EmailField, ReferenceField } from 'react-admin'; + +export const PostList = (props) => ( + + + + + + + + + + +); +``` + +When displaying the posts list, the app now fetches related user records, and displays their `name` as a ``. Notice the `label` property: you can use it on any field component to customize the field label. + +![reference posts in comment list](https://marmelab.com/react-admin/img/reference-posts.png) + +**Tip**: The `` component alone doesn't display anything. It just fetches the reference data, and passes it as a `record` to its child component. Just like the `` component, all `` components are only responsible for fetching and preparing data, and delegate rendering to their children. + +**Tip**: Look at the network tab of your browser again: react-admin deduplicates requests for users, and aggregates them in order to make only *one* HTTP request to the `/users` endpoint for the whole datagrid. That's one of many optimizations that keep the UI fast and responsive. + +## Adding Creation and Editing Capabilities + +An admin interface isn't just about displaying remote data, it should also allow creating and editing records. React-admin provides `` and `` components for that purpose. Add them to the `posts.js` script: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Edit, Create, Datagrid, ReferenceField, TextField, EditButton, DisabledInput, LongTextInput, ReferenceInput, SelectInput, SimpleForm, TextInput } from 'react-admin'; + +export const PostList = (props) => ( + + + + + + + + + + + +); + +const PostTitle = ({ record }) => { + return Post {record ? `"${record.title}"` : ''}; +}; + +export const PostEdit = (props) => ( + } {...props}> + + + + + + + + + +); + +export const PostCreate = (props) => ( + + + + + + + + + +); +``` + +If you've understood the `` component, the `` and `` components will be no surprise. They are responsible for fetching the record (or initializing an empty record in the case of ``), and displaying the page title. They pass the record down to the `` component, which is responsible for the form layout, default values, and validation. Just like ``, `` uses its children to determine the form inputs to display. It expects *input components* as children. ``, ``, ``, and `` are such inputs. + +As for the ``, it takes the same props as the `` (used earlier in the list page). `` uses these props to fetch the API for possible references related to the current record (in this case, possible `users` for the current `post`). It then passes these possible references to the child component (``), which is responsible for displaying them (via their `name` in that case), and letting the user select one. `` renders as a `` children. + +**Tip**: `` is a special component, which renders in two ways: + +- as a filter button (to add new filters) +- as a filter form (to enter filter values) + +It does so by inspecting its `context` prop. + +**Tip**: Don't mix up this `filters` prop, expecting a React element, with the `filter` props, which expects an object to define permanent filters (see below). + +The `Filter` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/overrides/#overriding-with-classes)). This property accepts the following keys: + +- `form`: applied to the root element when rendering as a form. +- `button`: applied to the root element when rendering as a button. + +Children of the `` form are regular inputs. `` hides them all by default, except those that have the `alwaysOn` prop. + +**Tip**: For technical reasons, react-admin does not accept children of `` having both a `defaultValue` and `alwaysOn`. To set default values for always on filters, use the `filterDefaultValues` prop of the `` component instead (see below). + +### Records Per Page + +By default, the list paginates results by groups of 10. You can override this setting by specifying the `perPage` prop: + +```jsx +// in src/posts.js +export const PostList = (props) => ( + + ... + +); +``` + +### Default Sort Field + +Pass an object literal as the `sort` prop to determine the default `field` and `order` used for sorting: + +```jsx +// in src/posts.js +export const PostList = (props) => ( + + ... + +); +``` + +`sort` defines the *default* sort order ; the list remains sortable by clicking on column headers. + +### Disabling Sorting + +It is possible to disable sorting for a specific field by passing a `sortable` property set to `false`: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Datagrid, TextField } from 'react-admin'; + +export const PostList = (props) => ( + + + + + + + +); +``` + +### Specify Sort Field + +By default, a column is sorted by the `source` property. To define another attribute to sort by, set it via the `sortBy` property: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Datagrid, TextField } from 'react-admin'; + +export const PostList = (props) => ( + + + + + + `${record.author.first_name} ${record.author.last_name}`} + /> + + + +); +``` + +### Permanent Filter + +You can choose to always filter the list, without letting the user disable this filter - for instance to display only published posts. Write the filter to be passed to the REST client in the `filter` props: + +```jsx +// in src/posts.js +export const PostList = (props) => ( + + ... + +); +``` + +The actual filter parameter sent to the REST client is the result of the combination of the *user* filters (the ones set through the `filters` component form), and the *permanent* filter. The user cannot override the permanent filters set by way of `filter`. + +### Filter Default Values + +To set default values to filters, you can either pass an object literal as the `filterDefaultValues` prop of the `` element, or use the `defaultValue` prop of any input component. + +There is one exception: inputs with `alwaysOn` don't accept `defaultValue`. You have to use the `filterDefaultValues` for those. + +```jsx +// in src/posts.js +const PostFilter = (props) => ( + + + + + +); + +export const PostList = (props) => ( + } filterDefaultValues={{ is_published: true }}> + ... + +); +``` + +**Tip**: The `filter` and `filterDefaultValues` props have one key difference: the `filterDefaultValues` can be overriddent by the user, while the `filter` values are always sent to the data provider. Or, to put it otherwise: + +```js +const filterSentToDataProvider = { ...filterDefaultValues, ...filterChosenByUser, ...filters }; +``` + +### Pagination + +You can replace the default pagination element by your own, using the `pagination` prop. The pagination element receives the current page, the number of records per page, the total number of records, as well as a `setPage()` function that changes the page. + +So if you want to replace the default pagination by a "" pagination, create a pagination component like the following: + +```jsx +import Button from '@material-ui/core/Button'; +import ChevronLeft from '@material-ui/icons/ChevronLeft'; +import ChevronRight from '@material-ui/icons/ChevronRight'; +import Toolbar from '@material-ui/core/Toolbar'; + +const PostPagination = ({ page, perPage, total, setPage }) => { + const nbPages = Math.ceil(total / perPage) || 1; + return ( + nbPages > 1 && + + {page > 1 && + + } + {page !== nbPages && + + } + + ); +} + +export const PostList = (props) => ( + }> + ... + +); +``` + +### CSS API + +The `List` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/overrides/#overriding-with-classes)). This property accepts the following keys: + +- `root`: alternative to using `className`. Applied to the root element. +- `header`: applied to the page header +- `actions`: applied to the actions container +- `noResults`: applied to the component shown when there is no result + +Here is an example of how you can override some of these classes: + +You can customize the list styles by passing a `classes` object as prop, through `withStyles()`. Here is an example: + +```jsx +const styles = { + header: { + backgroundColor: '#ccc', + }, +}; + +const PostList = ({ classes, ...props) => ( + + + ... + + +); + +export withStyles(styles)(PostList); +``` + +## The `Datagrid` component + +The datagrid component renders a list of records as a table. It is usually used as a child of the [``](#the-list-component) and [``](./Fields.md#referencemanyfield) components. + +Here are all the props accepted by the component: + +- [`rowStyle`](#row-style-function) + +It renders as many columns as it receives `` children. + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Datagrid, TextField, EditButton } from 'react-admin'; + +export const PostList = (props) => ( + + + + + + + + +); +``` + +The datagrid is an *iterator* component: it receives an array of ids, and a data store, and is supposed to iterate over the ids to display each record. Another example of iterator component is [``](#the-singlefieldlist-component). + +### Row Style Function + +You can customize the datagrid row style (applied to the `` element) based on the record, thanks to the `rowStyle` prop, which expects a function. + +For instance, this allows to apply a custom background to the entire row if one value of the record - like its number of views - passes a certain threshold. + +```jsx +const postRowStyle = (record, index) => ({ + backgroundColor: record.nb_views >= 500 ? '#efe' : 'white', +}); +export const PostList = (props) => ( + + + ... + + +); +``` + +### CSS API + +The `Datagrid` component accepts the usual `className` prop but you can override many class names injected to the inner components by React-admin thanks to the `classes` property (as most Material UI components, see their [documentation about it](https://material-ui.com/customization/overrides/#overriding-with-classes)). This property accepts the following keys: + +- `table`: alternative to using `className`. Applied to the root element. +- `tbody`: applied to the tbody +- `headerCell`: applied to each header cell +- `row`: applied to each row +- `rowEven`: applied to each even row +- `rowOdd`: applied to each odd row +- `rowCell`: applied to each row cell + +Here is an example of how you can override some of these classes: + +You can customize the datagrid styles by passing a `classes` object as prop, through `withStyles()`. Here is an example: + +```jsx +const styles = { + row: { + backgroundColor: '#ccc', + }, +}; + +const PostList = ({ classes, ...props) => ( + + + ... + + +); + +export withStyles(styles)(PostList); +``` + +**Tip**: If you want to override the `header` and `cell` styles independently for each column, use the `headerClassName` and `cellClassName` props in `` components. For instance, to hide a certain column on small screens: + +```jsx +import { withStyles } from '@material-ui/core/styles'; + +const styles = theme => ({ + hiddenOnSmallScreens: { + [theme.breakpoints.down('md')]: { + display: 'none', + }, + }, +}); + +const PostList = ({ classes, ...props }) => ( + + + + + + + +); + +export default withStyles(styles)(PostList); +``` + +## The `SimpleList` component + +For mobile devices, a `` is often unusable - there is simply not enough space to display several columns. The convention in that case is to use a simple list, with only one column per row. The `` component serves that purpose, leveraging [material-ui's `` and `` components](http://www.material-ui.com/#/components/list). You can use it as `` or `` child: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, SimpleList } from 'react-admin'; + +export const PostList = (props) => ( + + record.title} + secondaryText={record => `${record.views} views`} + tertiaryText={record => new Date(record.published_at).toLocaleDateString()} + /> + +); +``` + +`` iterates over the list data. For each record, it executes the `primaryText`, `secondaryText`, `leftAvatar`, `leftIcon`, `rightAvatar`, and `rightIcon` props function, and passes the result as the corresponding `` prop. + +**Tip**: To use a `` on small screens and a `` on larger screens, use the `` component: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Responsive, SimpleList, Datagrid, TextField, ReferenceField, EditButton } from 'react-admin'; + +export const PostList = (props) => ( + + record.title} + secondaryText={record => `${record.views} views`} + tertiaryText={record => new Date(record.published_at).toLocaleDateString()} + /> + } + medium={ + + ... + + } + /> + +); +``` + +**Tip**: The `` items link to the edition page by default. You can set the `linkType` prop to `show` to link to the `` page instead. + +```jsx +// in src/posts.js +import React from 'react'; +import { List, SimpleList } from 'react-admin'; + +export const PostList = (props) => ( + + record.title} + secondaryText={record => `${record.views} views`} + tertiaryText={record => new Date(record.published_at).toLocaleDateString()} + linkType="show" + /> + +); +``` + +Setting the `linkType` prop to `false` (boolean, not string) removes the link in all list items. + +## The `SingleFieldList` component + +When you want to display only one property of a list of records, instead of using a ``, use the ``. It expects a single `` as child. It's especially useful for `` or `` components: + +```jsx +// Display all the tags for the current post + + + + + +``` + +![ReferenceManyFieldSingleFieldList](https://marmelab.com/react-admin/img/reference-many-field-single-field-list.png) + +**Tip**: The `` items link to the edition page by default. You can set the `linkType` prop to `show` to link to the `` page instead. + +```jsx +// Display all the tags for the current post + + + + + +``` + +## Using a Custom Iterator + +A `` can delegate to any iterator component - `` is just one example. An iterator component must accept at least two props: + +- `ids` is an array of the ids currently displayed in the list +- `data` is an object of all the fetched data for this resource, indexed by id. + +For instance, what if you prefer to show a list of cards rather than a datagrid? + +![Custom iterator](https://marmelab.com/react-admin/img/custom-iterator.png) + +You'll need to create your own iterator component as follows: + +```jsx +// in src/comments.js +import Card from '@material-ui/core/Card'; +import CardActions from '@material-ui/core/CardActions'; +import CardContent from '@material-ui/core/CardContent'; +import CardHeader from '@material-ui/core/CardHeader'; + +const cardStyle = { + width: 300, + minHeight: 300, + margin: '0.5em', + display: 'inline-block', + verticalAlign: 'top' +}; +const CommentGrid = ({ ids, data, basePath }) => ( +
+ {ids.map(id => + + } + subheader={} + avatar={} />} + /> + + + + + about  + + + + + + + + + )} +
+); +CommentGrid.defaultProps = { + data: {}, + ids: [], +}; + +export const CommentList = (props) => ( + + + +); +``` + +As you can see, nothing prevents you from using `` components inside your own components... provided you inject the current `record`. Also, notice that components building links require the `basePath` component, which is also injected. + +## Displaying Fields depending on the user permissions + +You might want to display some fields or filters only to users with specific permissions. Those permissions are retrieved for each route and will provided to your component as a `permissions` prop. + +Each route will call the `authProvider` with the `AUTH_GET_PERMISSIONS` type and some parameters including the current location and route parameters. It's up to you to return whatever you need to check inside your component such as the user's role, etc. + +```jsx +const UserFilter = ({ permissions, ...props }) => + + + + {permissions === 'admin' ? : null} + ; + +export const UserList = ({ permissions, ...props }) => + } + sort={{ field: 'name', order: 'ASC' }} + > + record.name} + secondaryText={record => + permissions === 'admin' ? record.role : null} + /> + } + medium={ + + + + {permissions === 'admin' && } + {permissions === 'admin' && } + + + } + /> + ; +``` + +**Tip** Note how the `permissions` prop is passed down to the custom `filters` component. \ No newline at end of file From 1f21c49364f327c3879c6276fddbd2ac79af9a0c Mon Sep 17 00:00:00 2001 From: Kirk Date: Thu, 19 Jul 2018 23:15:11 +0800 Subject: [PATCH 10/20] New translations input-components.md (Chinese Simplified) --- .../translated_docs/zh-CN/input-components.md | 1284 +++++++++++++++++ 1 file changed, 1284 insertions(+) create mode 100644 website/translated_docs/zh-CN/input-components.md diff --git a/website/translated_docs/zh-CN/input-components.md b/website/translated_docs/zh-CN/input-components.md new file mode 100644 index 0000000..6e40938 --- /dev/null +++ b/website/translated_docs/zh-CN/input-components.md @@ -0,0 +1,1284 @@ +--- +id: input-components +title: components +--- +An `Input` component displays an input, or a dropdown list, a list of radio buttons, etc. Such components allow to edit a record property, and are common in the ``, ``, and `` views. + +```jsx +// in src/posts.js +import React from 'react'; +import { Edit, DisabledInput, LongTextInput, ReferenceInput, SelectInput, SimpleForm, TextInput } from 'react-admin'; + +export const PostEdit = (props) => ( + } {...props}> + + + + + + + + + +); +``` + +All input components accept the following attributes: + +* `source`: Property name of your entity to view/edit. This attribute is required. +* `defaultValue`: Value to be set when the property is `null` or `undefined`. +* `validate`: Validation rules for the current property (see the [Validation Documentation](./CreateEdit.html#validation)) +* `label`: Used as a table header of an input label. Defaults to the `source` when omitted. +* `style`: A style object to customize the look and feel of the field container (e.g. the `
` in a form). +* `elStyle`: A style object to customize the look and feel of the field element itself + +```jsx + +``` + +Additional props are passed down to the underlying component (usually a material-ui component). For instance, when setting the `fullWidth` prop on a `TextInput` component, the underlying material-ui `` receives it, and goes full width. + +**Tip**: If you edit a record with a complex structure, you can use a path as the `source` parameter. For instance, if the API returns the following 'book' record: + +```jsx +{ + id: 1234, + title: 'War and Peace', + author: { + firstName: 'Leo', + lastName: 'Tolstoi' + } +} +``` + +Then you can display a text input to edit the author first name as follows: + +```jsx + +``` + +**Tip**: If your interface has to support multiple languages, don't use the `label` prop, and put the localized labels in a dictionary instead. See the [Translation documentation](./Translation.md#translating-resource-and-field-names) for details. + +## `ArrayInput` Component + +To edit arrays of data embedded inside a record, `` creates a list of sub-forms. + +```jsx +import { ArrayInput, SimpleFormIterator, DateInput, UrlInput } from 'react-admin'; + + + + + + + +``` + +![ArrayInput](https://marmelab.com/react-admin/img/array-input.png) + +`` allows the edition of embedded arrays, like the `backlinks` field in the following `post` record: + +```js +{ + id: 123 + backlinks: [ + { + date: '2012-08-10T00:00:00.000Z', + url: 'http://example.com/foo/bar.html', + }, + { + date: '2012-08-14T00:00:00.000Z', + url: 'https://blog.johndoe.com/2012/08/12/foobar.html', + } + ] +} +``` + +`` expects a single child, which must be a *form iterator* component. A form iterator is a component accepting a `fields` object as passed by [redux-form's `` component](https://redux-form.com/7.3.0/examples/fieldarrays/), and defining a layout for an array of fields. For instance, the `` component displays an array of fields in an unordered list (`
    `), one sub-form by list item (`
  • `). It also provides controls for adding and removing a sub-record (a backlink in this example). + +You can pass `disableAdd` and `disableRemove` as props of `SimpleFormIterator`, to disable `ADD` and `REMOVE` button respectively. Default value of both is `false`. + +```jsx +import { ArrayInput, SimpleFormIterator, DateInput, UrlInput } from 'react-admin'; + + + + + + + +``` + +## `AutocompleteInput` Component + +To let users choose a value in a list using a dropdown with autocompletion, use ``. It renders using [react-autosuggest](http://react-autosuggest.js.org/) and a `fuzzySearch` filter. Set the `choices` attribute to determine the options list (with `id`, `name` tuples). + +```jsx +import { AutocompleteInput } from 'react-admin'; + + +``` + +You can also customize the properties to use for the option name and value, thanks to the `optionText` and `optionValue` attributes: + +```jsx +const choices = [ + { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, + { _id: 456, full_name: 'Jane Austen', sex: 'F' }, +]; + +``` + +`optionText` also accepts a function, so you can shape the option text at will: + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; + +``` + +The choices are translated by default, so you can use translation identifiers as choices: + +```jsx +const choices = [ + { id: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, +]; +``` + +However, in some cases (e.g. inside a ``), you may not want the choice to be translated. In that case, set the `translateChoice` prop to false. + +```jsx + +``` + +By default the component matches choices with the current input searchText: if it finds a match, this choice will be selected. For example, given the choices `[{ id: 'M', name: 'Male', id: 'F', name: 'Female' }]`, when the user enters the text `male`, then the component will set the input value to `M`. If you need to change how choices are matched, pass a custom function as `inputValueMatcher` prop. For example, given the choices: `[{id:1,iso2:'NL',name:'Dutch'},{id:2,iso2:'EN',name:'English'},{id:3,iso2:'FR',name:'French'}]`, if you want to match choices on the iso2 code, you can create the following `inputValueMatcher` function: + +```javascript + + input.toUpperCase().trim() === suggestion.iso2 || + input.toLowerCase().trim() === getOptionText(suggestion).toLowerCase().trim() +}/> +``` + +If you want to limit the initial choices shown to the current value only, you can set the `limitChoicesToValue` prop. + +Lastly, `` renders a meterial-ui `` component. Use the `options` attribute to override any of the `` attributes: + +{% raw %} + +```jsx + +``` + +{% endraw %} + +**Tip**: If you want to populate the `choices` attribute with a list of related records, you should decorate `` with [``](#referenceinput), and leave the `choices` empty: + +```jsx +import { AutocompleteInput, ReferenceInput } from 'react-admin' + + + + +``` + +**Tip**: `` is a stateless component, so it only allows to *filter* the list of choices, not to *extend* it. If you need to populate the list of choices based on the result from a `fetch` call (and if [``](#referenceinput) doesn't cover your need), you'll have to [write your own Input component](#writing-your-own-input-component) based on material-ui `` component. + +**Tip**: React-admin's `` has only a capital A, while material-ui's `` has a capital A and a capital C. Don't mix up the components! + +### Properties + +| Prop | Required | Type | Default | Description | +| --------------------- | -------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `choices` | Required | `Object[]` | - | List of items to autosuggest | +| `resource` | Required | `string` | - | The resource working on. This field is passed down by wrapped components like `Create` and `Edit`. | +| `source` | Required | `string` | - | Name of field to edit, its type should match the type retrieved from `optionValue` | +| `allowEmpty` | Optional | `boolean` | `false` | If `false` and the searchText typed did not match any suggestion, the searchText will revert to the current value when the field is blurred. If `true` and the `searchText` is set to `''` then the field will set the input value to `null`. | +| `inputValueMatcher` | Optional | `Function` | `(input, suggestion, getOptionText) => input.toLowerCase().trim() === getOptionText(suggestion).toLowerCase().trim()` | Allows to define how choices are matched with the searchText while typing. | +| `optionValue` | Optional | `string` | `id` | Fieldname of record containing the value to use as input value | +| `optionText` | Optional | `string | Function` | `name` | Fieldname of record to display in the suggestion item or function which accepts the currect record as argument (`(record)=> {string}`) | +| `setFilter` | Optional | `Function` | null | A callback to inform the `searchText` has changed and new `choices` can be retrieved based on this `searchText`. Signature `searchText => void`. This function is automatically setup when using `ReferenceInput`. | +| `suggestionComponent` | Optional | Function | `({ suggestion, query, isHighlighted, props }) =>
    ` | Allows to override how the item is rendered. | + +## `BooleanInput` and `NullableBooleanInput` Component + +`` is a toggle button allowing you to attribute a `true` or `false` value to a record field. + +```jsx +import { BooleanInput } from 'react-admin'; + + +``` + +![BooleanInput](https://marmelab.com/react-admin/img/boolean-input.png) + +This input does not handle `null` values. You would need the `` component if you have to handle non-set booleans. + +You can use the `options` prop to pass any option supported by the Material UI `Switch` components. For example, here's how to set a custom checked icon: + +{% raw %} + +```jsx +import { BooleanInput } from 'react-admin'; +import FavoriteIcon from '@material-ui/icons/Favorite'; + +, + }} +/> +``` + +{% endraw %} + +![CustomBooleanInputCheckIcon](https://marmelab.com/react-admin/img/custom-switch-icon.png) + +Refer to [Material UI Switch documentation](http://www.material-ui.com/#/components/switch) for more details. + +`` renders as a dropdown list, allowing to choose between true, false, and null values. + +```jsx +import { NullableBooleanInput } from 'react-admin'; + + +``` + +![NullableBooleanInput](https://marmelab.com/react-admin/img/nullable-boolean-input.png) + +## `CheckboxGroupInput` Component + +If you want to let the user choose multiple values among a list of possible values by showing them all, `` is the right component. Set the `choices` attribute to determine the options (with `id`, `name` tuples): + +```jsx +import { CheckboxGroupInput } from 'react-admin'; + + +``` + +![CheckboxGroupInput](https://marmelab.com/react-admin/img/checkbox-group-input.png) + +You can also customize the properties to use for the option name and value, thanks to the `optionText` and `optionValue` attributes: + +```jsx +const choices = [ + { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, + { _id: 456, full_name: 'Jane Austen', sex: 'F' }, +]; + +``` + +`optionText` also accepts a function, so you can shape the option text at will: + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; + +``` + +`optionText` also accepts a React Element, that will be cloned and receive the related choice as the `record` prop. You can use Field components there. + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const FullNameField = ({ record }) => {record.first_name} {record.last_name}; +}/> +``` + +The choices are translated by default, so you can use translation identifiers as choices: + +```jsx +const choices = [ + { id: 'programming', name: 'myroot.category.programming' }, + { id: 'lifestyle', name: 'myroot.category.lifestyle' }, + { id: 'photography', name: 'myroot.category.photography' }, +]; +``` + +However, in some cases (e.g. inside a ``), you may not want the choice to be translated. In that case, set the `translateChoice` prop to false. + +```jsx + +``` + +Lastly, use the `options` attribute if you want to override any of Material UI's `` attributes: + +{% raw %} + +```jsx + +``` + +{% endraw %} + +Refer to [Material UI Checkbox documentation](http://www.material-ui.com/#/components/checkbox) for more details. + +## `DateInput` Component + +Ideal for editing dates, `` renders a standard browser [Date Picker](http://www.material-ui.com/#/components/date-picker). + +```jsx +import { DateInput } from 'react-admin'; + + +``` + +![DateInput](./img/date-input.gif) + +## `DisabledInput` Component + +When you want to display a record property in an `` form without letting users update it (such as for auto-incremented primary keys), use the ``: + +```jsx +import { DisabledInput } from 'react-admin'; + + +``` + +![DisabledInput](https://marmelab.com/react-admin/img/disabled-input.png) + +**Tip**: To add non-editable fields to the `` view, you can also use one of react-admin `Field` components: + +```jsx +// in src/posts.js +import { Edit, LongTextInput, SimpleForm, TextField } from 'react-admin'; + +export const PostEdit = (props) => ( + + + {/* NOT EDITABLE */} + + + +); +``` + +**Tip**: You can even use a component of your own, provided it accepts a `record` prop: + +```jsx +// in src/posts.js +import { Edit, Labeled, LongTextInput, SimpleForm } from 'react-admin'; + +const titleStyle = { textOverflow: 'ellipsis', overflow: 'hidden', maxWidth: '20em' }; +const Title = ({ record, label }) => ( + + {record.title} + +); + +export const PostEdit = (props) => ( + + + + <LongTextInput source="body" /> + </SimpleForm> + </Edit> +); +``` + +## `ImageInput` Component + +`<ImageInput>` allows to upload some pictures using [react-dropzone](https://github.com/okonet/react-dropzone). + +![ImageInput](https://marmelab.com/react-admin/img/image-input.png) + +Previews are enabled using `<ImageInput>` children, as following: + +```jsx +<ImageInput source="pictures" label="Related pictures" accept="image/*"> + <ImageField source="src" title="title" /> +</ImageInput> +``` + +Writing a custom field component for displaying the current value(s) is easy: it's a standard [field](./Fields.md#writing_your_own_field_component). + +When receiving **new** files, `ImageInput` will add a `rawFile` property to the object passed as the `record` prop of children. This `rawFile` is the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) instance of the newly added file. This can be useful to display informations about size or mimetype inside a custom field. + +The `ImageInput` component accepts all [react-dropzone properties](https://github.com/okonet/react-dropzone#features), in addition to those of react-admin. For instance, if you need to upload several images at once, just add the `multiple` DropZone attribute to your `<ImageInput />` field. + +If the default Dropzone label doesn't fit with your need, you can pass a `placeholder` attribute to overwrite it. The attribute can be anything React can render (`PropTypes.node`): + +```jsx +<ImageInput source="pictures" label="Related pictures" accept="image/*" placeholder={<p>Drop your file here</p>}> + <ImageField source="src" title="title" /> +</ImageInput> +``` + +Note that the image upload returns a [File](https://developer.mozilla.org/en/docs/Web/API/File) object. It is your responsibility to handle it depending on your API behavior. You can for instance encode it in base64, or send it as a multi-part form data. Check [this example](./DataProviders.md#decorating-your-rest-client-example-of-file-upload) for base64 encoding data by extending the REST Client. + +## `FileInput` Component + +`<FileInput>` allows to upload some files using [react-dropzone](https://github.com/okonet/react-dropzone). + +![FileInput](https://marmelab.com/react-admin/img/file-input.png) + +Previews (actually a simple list of files names) are enabled using `<FileInput>` children, as following: + +```jsx +<FileInput source="files" label="Related files" accept="application/pdf"> + <FileField source="src" title="title" /> +</FileInput> +``` + +Writing a custom field component for displaying the current value(s) is easy: it's a standard [field](./Fields.md#writing_your_own_field_component). + +When receiving **new** files, `FileInput` will add a `rawFile` property to the object passed as the `record` prop of children. This `rawFile` is the [File](https://developer.mozilla.org/en-US/docs/Web/API/File) instance of the newly added file. This can be useful to display informations about size or mimetype inside a custom field. + +The `FileInput` component accepts all [react-dropzone properties](https://github.com/okonet/react-dropzone#features), in addition to those of react-admin. For instance, if you need to upload several files at once, just add the `multiple` DropZone attribute to your `<FileInput />` field. + +If the default Dropzone label doesn't fit with your need, you can pass a `placeholder` attribute to overwrite it. The attribute can be anything React can render (`PropTypes.node`): + +```jsx +<FileInput source="files" label="Related files" accept="application/pdf" placeholder={<p>Drop your file here</p>}> + <ImageField source="src" title="title" /> +</FileInput> +``` + +Note that the file upload returns a [File](https://developer.mozilla.org/en/docs/Web/API/File) object. It is your responsibility to handle it depending on your API behavior. You can for instance encode it in base64, or send it as a multi-part form data. Check [this example](./DataProviders.md#decorating-your-rest-client-example-of-file-upload) for base64 encoding data by extending the REST Client. + +## `LongTextInput` Component + +`<LongTextInput>` is the best choice for multiline text values. It renders as an auto expandable textarea. + +```jsx +import { LongTextInput } from 'react-admin'; + +<LongTextInput source="teaser" /> +``` + +![LongTextInput](https://marmelab.com/react-admin/img/long-text-input.png) + +## `NumberInput` Component + +`<NumberInput>` translates to a HTML `<input type="number">`. It is necessary for numeric values because of a [known React bug](https://github.com/facebook/react/issues/1425), which prevents using the more generic [`<TextInput>`](#textinput) in that case. + +```jsx +import { NumberInput } from 'react-admin'; + +<NumberInput source="nb_views" /> +``` + +You can customize the `step` props (which defaults to "any"): + +```jsx +<NumberInput source="nb_views" step={1} /> +``` + +## `RadioButtonGroupInput` Component + +If you want to let the user choose a value among a list of possible values by showing them all (instead of hiding them behind a dropdown list, as in [`<SelectInput>`](#selectinput)), `<RadioButtonGroupInput>` is the right component. Set the `choices` attribute to determine the options (with `id`, `name` tuples): + +```jsx +import { RadioButtonGroupInput } from 'react-admin'; + +<RadioButtonGroupInput source="category" choices={[ + { id: 'programming', name: 'Programming' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'photography', name: 'Photography' }, +]} /> +``` + +![RadioButtonGroupInput](https://marmelab.com/react-admin/img/radio-button-group-input.png) + +You can also customize the properties to use for the option name and value, thanks to the `optionText` and `optionValue` attributes: + +```jsx +const choices = [ + { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, + { _id: 456, full_name: 'Jane Austen', sex: 'F' }, +]; +<RadioButtonGroupInput source="author_id" choices={choices} optionText="full_name" optionValue="_id" /> +``` + +`optionText` also accepts a function, so you can shape the option text at will: + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; +<RadioButtonGroupInput source="author_id" choices={choices} optionText={optionRenderer} /> +``` + +`optionText` also accepts a React Element, that will be cloned and receive the related choice as the `record` prop. You can use Field components there. + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const FullNameField = ({ record }) => <span>{record.first_name} {record.last_name}</span>; +<RadioButtonGroupInput source="gender" choices={choices} optionText={<FullNameField />}/> +``` + +The choices are translated by default, so you can use translation identifiers as choices: + +```jsx +const choices = [ + { id: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, +]; +``` + +However, in some cases (e.g. inside a `<ReferenceInput>`), you may not want the choice to be translated. In that case, set the `translateChoice` prop to false. + +```jsx +<RadioButtonGroupInput source="gender" choices={choices} translateChoice={false}/> +``` + +Lastly, use the `options` attribute if you want to override any of Material UI's `<RadioButtonGroup>` attributes: + +{% raw %} + +```jsx +<RadioButtonGroupInput source="category" options={{ + labelPosition: 'right' +}} /> +``` + +{% endraw %} + +Refer to [Material UI RadioGroup documentation](http://www.material-ui.com/#/components/radio-button) for more details. + +**Tip**: If you want to populate the `choices` attribute with a list of related records, you should decorate `<RadioButtonGroupInput>` with [`<ReferenceInput>`](#referenceinput), and leave the `choices` empty: + +```jsx +import { RadioButtonGroupInput, ReferenceInput } from 'react-admin' + +<ReferenceInput label="Author" source="author_id" reference="authors"> + <RadioButtonGroupInput optionText="last_name" /> +</ReferenceInput> +``` + +## `ReferenceArrayInput` Component + +Use `<ReferenceArrayInput>` to edit an array of reference values, i.e. to let users choose a list of values (usually foreign keys) from another REST endpoint. + +`<ReferenceArrayInput>` fetches the related resources (using the `CRUD_GET_MANY` REST method) as well as possible resources (using the `CRUD_GET_MATCHING` REST method) in the reference endpoint. + +For instance, if the post object has many tags, a post resource may look like: + +```js +{ + id: 1234, + tag_ids: [1, 23, 4] +} +``` + +Then `<ReferenceArrayInput>` would fetch a list of tag resources from these two calls: + + http://myapi.com/tags?id=[1,23,4] + http://myapi.com/tags?page=1&perPage=25 + + +Once it receives the deduplicated reference resources, this component delegates rendering to a subcomponent, to which it passes the possible choices as the `choices` attribute. + +This means you can use `<ReferenceArrayInput>` with [`<SelectArrayInput>`](#selectarrayinput), or with the component of your choice, provided it supports the `choices` attribute. + +The component expects a `source` and a `reference` attributes. For instance, to make the `tag_ids` for a `post` editable: + +```js +import { ReferenceArrayInput, SelectArrayInput } from 'react-admin' + +<ReferenceArrayInput source="tag_ids" reference="tags"> + <SelectArrayInput optionText="name" /> +</ReferenceArrayInput> +``` + +![SelectArrayInput](https://marmelab.com/react-admin/img/select-array-input.gif) + +**Note**: You **must** add a `<Resource>` for the reference resource - react-admin needs it to fetch the reference data. You can omit the list prop in this reference if you want to hide it in the sidebar menu. + +```js +<Admin dataProvider={myDataProvider}> + <Resource name="posts" list={PostList} edit={PostEdit} /> + <Resource name="tags" /> +</Admin> +``` + +Set the `allowEmpty` prop when you want to add an empty choice with a value of null in the choices list. Disabling `allowEmpty` does not mean that the input will be required. If you want to make the input required, you must add a validator as indicated in [Validation Documentation](./CreateEdit.html#validation). Enabling the `allowEmpty` props just adds an empty choice (with `null` value) on top of the options, and makes the value nullable. + +```js +import { ReferenceArrayInput, SelectArrayInput } from 'react-admin' + +<ReferenceArrayInput source="tag_ids" reference="tags" allowEmpty> + <SelectArrayInput optionText="name" /> +</ReferenceArrayInput> +``` + +**Tip**: `allowEmpty` is set by default for all Input components children of the `<Filter>` component + +You can tweak how this component fetches the possible values using the `perPage`, `sort`, and `filter` props. + +{% raw %} + +```js +// by default, fetches only the first 25 values. You can extend this limit +// by setting the `perPage` prop. +<ReferenceArrayInput + source="tag_ids" + reference="tags" + perPage={100}> + <SelectArrayInput optionText="name" /> +</ReferenceArrayInput> + +// by default, orders the possible values by id desc. You can change this order +// by setting the `sort` prop (an object with `field` and `order` properties). +<ReferenceArrayInput + source="tag_ids" + reference="tags" + sort={{ field: 'title', order: 'ASC' }}> + <SelectArrayInput optionText="name" /> +</ReferenceArrayInput> + +// you can filter the query used to populate the possible values. Use the +// `filter` prop for that. +<ReferenceArrayInput + source="tag_ids" + reference="tags" + filter={{ is_published: true }}> + <SelectArrayInput optionText="name" /> +</ReferenceArrayInput> +``` + +{% endraw %} + +## `ReferenceInput` Component + +Use `<ReferenceInput>` for foreign-key values, for instance, to edit the `post_id` of a `comment` resource. This component fetches the possible values in the reference resource (using the `GET_LIST` REST method) and the referenced record (using the `GET_ONE` REST method), then delegates rendering to a subcomponent, to which it passes the possible choices as the `choices` attribute. + +This means you can use `<ReferenceInput>` with any of [`<SelectInput>`](#selectinput), [`<AutocompleteInput>`](#autocompleteinput), or [`<RadioButtonGroupInput>`](#radiobuttongroupinput), or even with the component of your choice, provided it supports the `choices` attribute. + +The component expects a `source` and a `reference` attributes. For instance, to make the `post_id` for a `comment` editable: + +```jsx +import { ReferenceInput, SelectInput } from 'react-admin' + +<ReferenceInput label="Post" source="post_id" reference="posts"> + <SelectInput optionText="title" /> +</ReferenceInput> +``` + +![ReferenceInput](https://marmelab.com/react-admin/img/reference-input.gif) + +**Note**: You **must** add a `<Resource>` for the reference resource - react-admin needs it to fetch the reference data. You *can* omit the `list` prop in this reference if you want to hide it in the sidebar menu. + +```jsx +<Admin dataProvider={myDataProvider}> + <Resource name="comments" list={CommentList} /> + <Resource name="posts" /> +</Admin> +``` + +Set the `allowEmpty` prop when you want to add an empty choice with a value of null in the choices list. Disabling `allowEmpty` does not mean that the input will be required. If you want to make the input required, you must add a validator as indicated in [Validation Documentation](./CreateEdit.html#validation). Enabling the `allowEmpty` props just adds an empty choice (with `null` value) on top of the options, and makes the value nullable. + +```jsx +import { ReferenceInput, SelectInput } from 'react-admin' + +<ReferenceInput label="Post" source="post_id" reference="posts" allowEmpty> + <SelectInput optionText="title" /> +</ReferenceInput> +``` + +**Tip**: `allowEmpty` is set by default for all Input components children of the `<Filter>` component: + +```jsx +const CommentFilter = (props) => ( + <Filter {...props}> + <ReferenceInput label="Post" source="post_id" reference="posts"> // no need for allowEmpty + <SelectInput optionText="title" /> + </ReferenceInput> + </Filter> +); +``` + +You can tweak how this component fetches the possible values using the `perPage`, `sort`, and `filter` props. + +{% raw %} + +```jsx +// by default, fetches only the first 25 values. You can extend this limit +// by setting the `perPage` prop. +<ReferenceInput + source="post_id" + reference="posts" + perPage={100}> + <SelectInput optionText="title" /> +</ReferenceInput> + +// by default, orders the possible values by id desc. You can change this order +// by setting the `sort` prop (an object with `field` and `order` properties). +<ReferenceInput + source="post_id" + reference="posts" + sort={{ field: 'title', order: 'ASC' }}> + <SelectInput optionText="title" /> +</ReferenceInput> + +// you can filter the query used to populate the possible values. Use the +// `filter` prop for that. +<ReferenceInput + source="post_id" + reference="posts" + filter={{ is_published: true }}> + <SelectInput optionText="title" /> +</ReferenceInput> +``` + +{% endraw %} + +The child component may further filter results (that's the case, for instance, for `<AutocompleteInput>`). ReferenceInput passes a `setFilter` function as prop to its child component. It uses the value to create a filter for the query - by default `{ q: [searchText] }`. You can customize the mapping `searchText => searchQuery` by setting a custom `filterToQuery` function prop: + +```jsx +<ReferenceInput + source="post_id" + reference="posts" + filterToQuery={searchText => ({ title: searchText })}> + <SelectInput optionText="title" /> +</ReferenceInput> +``` + +The child component receives the following props from `<ReferenceInput>`: + +* `isLoading`: whether the request for possible values is loading or not +* `filter`: the current filter of the request for possible values. Defaults to `{}`. +* `pagination`: the current pagination of the request for possible values. Defaults to `{ page: 1, perPage: 25 }`. +* `sort`: the current sorting of the request for possible values. Defaults to `{ field: 'id', order: 'DESC' }`. +* `error`: the error message if the form validation failed for that input +* `warning`: the warning message if the form validation failed for that input +* `onChange`: function to call when the value changes +* `setFilter`: function to call to update the filter of the request for possible values +* `setPagination`: : function to call to update the pagination of the request for possible values +* `setSort`: function to call to update the sorting of the request for possible values + +## `RichTextInput` Component + +`<RichTextInput>` is the ideal component if you want to allow your users to edit some HTML contents. It is powered by [Quill](https://quilljs.com/). + +**Note**: Due to its size, `<RichTextInput>` is not bundled by default with react-admin. You must install it first, using npm: + +```sh +npm install ra-input-rich-text +``` + +Then use it as a normal input component: + +```jsx +import RichTextInput from 'ra-input-rich-text'; + +<RichTextInput source="body" /> +``` + +![RichTextInput](https://marmelab.com/react-admin/img/rich-text-input.png) + +You can customize the rich text editor toolbar using the `toolbar` attribute, as described on the [Quill official toolbar documentation](https://quilljs.com/docs/modules/toolbar/). + +```jsx +<RichTextInput source="body" toolbar={[ ['bold', 'italic', 'underline', 'link'] ]} /> +``` + +## `SelectInput` Component + +To let users choose a value in a list using a dropdown, use `<SelectInput>`. It renders using [Material ui's `<SelectField>`](http://www.material-ui.com/#/components/select-field). Set the `choices` attribute to determine the options (with `id`, `name` tuples): + +```jsx +import { SelectInput } from 'react-admin'; + +<SelectInput source="category" choices={[ + { id: 'programming', name: 'Programming' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'photography', name: 'Photography' }, +]} /> +``` + +![SelectInput](https://marmelab.com/react-admin/img/select-input.gif) + +You can also customize the properties to use for the option name and value, thanks to the `optionText` and `optionValue` attributes: + +```jsx +const choices = [ + { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, + { _id: 456, full_name: 'Jane Austen', sex: 'F' }, +]; +<SelectInput source="author_id" choices={choices} optionText="full_name" optionValue="_id" /> +``` + +`optionText` also accepts a function, so you can shape the option text at will: + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; +<SelectInput source="author_id" choices={choices} optionText={optionRenderer} /> +``` + +`optionText` also accepts a React Element, that will be cloned and receive the related choice as the `record` prop. You can use Field components there. + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const FullNameField = ({ record }) => <span>{record.first_name} {record.last_name}</span>; +<SelectInput source="gender" choices={choices} optionText={<FullNameField />}/> +``` + +Enabling the `allowEmpty` props adds an empty choice (with `null` value) on top of the options, and makes the value nullable: + +```jsx +<SelectInput source="category" allowEmpty choices={[ + { id: 'programming', name: 'Programming' }, + { id: 'lifestyle', name: 'Lifestyle' }, + { id: 'photography', name: 'Photography' }, +]} /> +``` + +The choices are translated by default, so you can use translation identifiers as choices: + +```jsx +const choices = [ + { id: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, +]; +``` + +However, in some cases, you may not want the choice to be translated. In that case, set the `translateChoice` prop to false. + +```jsx +<SelectInput source="gender" choices={choices} translateChoice={false}/> +``` + +Note that `translateChoice` is set to false when `<SelectInput>` is a child of `<ReferenceInput>`. + +Lastly, use the `options` attribute if you want to override any of Material UI's `<SelectField>` attributes: + +{% raw %} + +```jsx +<SelectInput source="category" options={{ + maxHeight: 200 +}} /> +``` + +{% endraw %} + +Refer to [Material UI SelectField documentation](http://www.material-ui.com/#/components/select-field) for more details. + +**Tip**: If you want to populate the `choices` attribute with a list of related records, you should decorate `<SelectInput>` with [`<ReferenceInput>`](#referenceinput), and leave the `choices` empty: + +```jsx +import { SelectInput, ReferenceInput } from 'react-admin' + +<ReferenceInput label="Author" source="author_id" reference="authors"> + <SelectInput optionText="last_name" /> +</ReferenceInput> +``` + +If, instead of showing choices as a dropdown list, you prefer to display them as a list of radio buttons, try the [`<RadioButtonGroupInput>`](#radiobuttongroupinput). And if the list is too big, prefer the [`<AutocompleteInput>`](#autocompleteinput). + +## `SelectArrayInput` Component + +To let users choose several values in a list using a dropdown, use `<SelectArrayInput>`. It renders using [Material ui's `<Select>`](http://www.material-ui.com/#/components/select). Set the `choices` attribute to determine the options (with `id`, `name` tuples): + +```js +import { SelectArrayInput } from 'react-admin'; + +<SelectArrayInput label="Tags" source="categories" choices={[ + { id: 'music', name: 'Music' }, + { id: 'photography', name: 'Photo' }, + { id: 'programming', name: 'Code' }, + { id: 'tech', name: 'Technology' }, + { id: 'sport', name: 'Sport' }, +]} /> +``` + +![SelectArrayInput](https://marmelab.com/react-admin/img/select-array-input.gif) + +You can also customize the properties to use for the option name and value, thanks to the `optionText` and `optionValue` attributes. + +```js +const choices = [ + { _id: '1', name: 'Book', plural_name: 'Books' }, + { _id: '2', name: 'Video', plural_name: 'Videos' }, + { _id: '3', name: 'Audio', plural_name: 'Audios' }, +]; +<SelectArrayInput source="categories" choices={choices} optionText="plural_name" optionValue="_id" /> +``` + +`optionText` also accepts a function, so you can shape the option text at will: + +```js +const choices = [ + { id: '1', name: 'Book', quantity: 23 }, + { id: '2', name: 'Video', quantity: 56 }, + { id: '3', name: 'Audio', quantity: 12 }, +]; +const optionRenderer = choice => `${choice.name} (${choice.quantity})`; +<SelectArrayInput source="categories" choices={choices} optionText={optionRenderer} /> +``` + +The choices are translated by default, so you can use translation identifiers as choices: + +```js +const choices = [ + { id: 'books', name: 'myroot.category.books' }, + { id: 'sport', name: 'myroot.category.sport' }, +]; +``` + +Lastly, use the `options` attribute if you want to override any of the `<Select>` attributes: + +{% raw %} + +```js +<SelectArrayInput source="category" options={{ fullWidth: true }} /> +``` + +{% endraw %} + +Refer to [the Select documentation](http://www.material-ui.com/#/components/select) for more details. + +The `SelectArrayInput` component **cannot** be used inside a `ReferenceInput` but can be used inside a `ReferenceArrayInput`. + +```jsx +import React from 'react'; +import { + ChipField, + Create, + DateInput, + LongTextInput, + ReferenceArrayInput, + SelectArrayInput, + TextInput, +} from 'react-admin'; + +export const PostCreate = props => ( + <Create {...props}> + <SimpleForm> + <TextInput source="title" /> + <LongTextInput source="body" /> + <DateInput source="published_at" /> + + <ReferenceArrayInput reference="tags" source="tags"> + <SelectArrayInput> + <ChipField source="name" /> + </SelectArrayInput> + </ReferenceArrayInput> + </SimpleForm> + </Create> +); +``` + +**Tip**: As it does not provide autocompletion, the `SelectArrayInput` might not be suited when the referenced resource has a lot of items. + +## `TextInput` Component + +`<TextInput>` is the most common input. It is used for texts, emails, URL or passwords. In translates to an HTML `<input>` tag. + +```jsx +import { TextInput } from 'react-admin'; + +<TextInput source="title" /> +``` + +![TextInput](https://marmelab.com/react-admin/img/text-input.png) + +You can choose a specific input type using the `type` attribute, for instance `text` (the default), `email`, `url`, or `password`: + +```jsx +<TextInput label="Email Address" source="email" type="email" /> +``` + +**Warning**: Do not use `type="number"`, or you'll receive a string as value (this is a [known React bug](https://github.com/facebook/react/issues/1425)). Instead, use [`<NumberInput>`](#numberinput). + +## Transforming Input Value to/from Record + +The data format returned by the input component may not be what your API desires. Since React-admin uses Redux Form, we can use its `parse()` and `format()` functions to transform the input value when saving to and loading from the record. It's better to understand the [input value's lifecycle](http://redux-form.com/6.5.0/docs/ValueLifecycle.md/) before you start. + +Mnemonic for the two functions: - `parse()`: input -> record - `format()`: record -> input + +Say the user would like to input values of 0-100 to a percentage field but your API (hence record) expects 0-1.0. You can use simple `parse()` and `format()` functions to archive the transform: + +```jsx +<NumberInput source="percent" format={v => v*100} parse={v => v/100} label="Formatted number" /> +``` + +`<DateInput>` stores and returns a string. If you would like to store a JavaScript Date object in your record instead: + +```jsx +const dateFormatter = v => { + // v is a `Date` object + if (!(v instanceof Date) || isNaN(v)) return; + const pad = '00'; + const yy = v.getFullYear().toString(); + const mm = (v.getMonth() + 1).toString(); + const dd = v.getDate().toString(); + return `${yy}-${(pad + mm).slice(-2)}-${(pad + dd).slice(-2)}`; +}; + +const dateParser = v => { + // v is a string of "YYYY-MM-DD" format + const match = /(\d{4})-(\d{2})-(\d{2})/.exec(v); + if (match === null) return; + const d = new Date(match[1], parseInt(match[2], 10) - 1, match[3]); + if (isNaN(d)) return; + return d; +}; + +<DateInput source="isodate" format={dateFormatter} parse={dateParser} /> +``` + +## Third-Party Components + +You can find components for react-admin in third-party repositories. + +* [vascofg/react-admin-color-input](https://github.com/vascofg/react-admin-color-input): a color input using [React Color](http://casesandberg.github.io/react-color/), a collection of color pickers. +* [LoicMahieu/aor-tinymce-input](https://github.com/LoicMahieu/aor-tinymce-input): a TinyMCE component, useful for editing HTML +* [vascofg/react-admin-date-inputs](https://github.com/vascofg/react-admin-date-inputs): a collection of Date Inputs, based on [material-ui-pickers](https://material-ui-pickers.firebaseapp.com/) + +## Writing Your Own Input Component + +If you need a more specific input type, you can also write it yourself. You'll have to rely on redux-form's [`<Field>`](http://redux-form.com/6.5.0/docs/api/Field.md/) component, so as to handle the value update cycle. + +For instance, let's write a component to edit the latitude and longitude of the current record: + +```jsx +// in LatLongInput.js +import { Field } from 'redux-form'; +const LatLngInput = () => ( + <span> + <Field name="lat" component="input" type="number" placeholder="latitude" /> +   + <Field name="lng" component="input" type="number" placeholder="longitude" /> + </span> +); +export default LatLngInput; + +// in ItemEdit.js +const ItemEdit = (props) => ( + <Edit {...props}> + <SimpleForm> + <LatLngInput /> + </SimpleForm> + </Edit> +); +``` + +`LatLngInput` takes no props, because the `<Field>` component can access the current record via its context. The `name` prop serves as a selector for the record property to edit. All `Field` props except `name` and `component` are passed to the child component/element (an `<input>` in that example). Executing this component will render roughly the following code: + +```html +<span> + <input type="number" placeholder="longitude" value={record.lat} /> + <input type="number" placeholder="longitude" value={record.lng} /> +</span> +``` + +**Tip**: The `<Field>` component supports dot notation in the `name` prop, to edit nested props: + +```jsx +const LatLongInput = () => ( + <span> + <Field name="position.lat" component="input" type="number" placeholder="latitude" /> +   + <Field name="position.lng" component="input" type="number" placeholder="longitude" /> + </span> +); +``` + +This component lacks a label. React-admin provides the `<Labeled>` component for that: + +```jsx +// in LatLongInput.js +import { Field } from 'redux-form'; +import { Labeled } from 'react-admin'; + +const LatLngInput = () => ( + <Labeled label="position"> + <span> + <Field name="lat" component="input" type="number" placeholder="latitude" /> +   + <Field name="lng" component="input" type="number" placeholder="longitude" /> + </span> + </Labeled> +); +export default LatLngInput; +``` + +Now the component will render with a label: + +```html +<label>Position</label> +<span> + <input type="number" placeholder="longitude" value={record.lat} /> + <input type="number" placeholder="longitude" value={record.lng} /> +</span> +``` + +Instead of HTML `input` elements, you can use a material-ui component. To compose material-ui and `Field`, use a [field renderer function](http://redux-form.com/6.5.0/examples/material-ui/) to map the props: + +```jsx +// in LatLongInput.js +import TextField from '@material-ui/core/TextField'; +import { Field } from 'redux-form'; +const renderTextField = ({ input, label, meta: { touched, error }, ...custom }) => ( + <TextField + hintText={label} + floatingLabelText={label} + errorText={touched && error} + {...input} + {...custom} + /> +); +const LatLngInput = () => ( + <span> + <Field name="lat" component={renderTextField} label="latitude" /> +   + <Field name="lng" component={renderTextField} label="longitude" /> + </span> +); +``` + +Material-ui's `<TextField>` component already includes a label, so you don't need to use `<Labeled>` in this case. `<Field>` injects two props to its child component: `input` and `meta`. To learn more about these props, please refer to [the `<Field>` component documentation](http://redux-form.com/6.5.0/docs/api/Field.md/#props) in the redux-form website. + +**Tip**: If you only need one `<Field>` component in a custom input, you can let react-admin do the `<Field>` decoration for you by using the `addField` Higher-order component: + +```jsx +// in SexInput.js +import SelectField from '@material-ui/core/SelectField'; +import MenuItem from '@material-ui/core/MenuItem'; +import { addField } from 'react-admin'; + +const SexInput = ({ input, meta: { touched, error } }) => ( + <SelectField + floatingLabelText="Sex" + errorText={touched && error} + {...input} + > + <MenuItem value="M" primaryText="Male" /> + <MenuItem value="F" primaryText="Female" /> + </SelectField> +); +export default addField(SexInput); // decorate with redux-form's <Field> + +// equivalent of +import SelectField from '@material-ui/core/SelectField'; +import MenuItem from '@material-ui/core/MenuItem'; +import { Field } from 'redux-form'; + +const renderSexInput = ({ input, meta: { touched, error } }) => ( + <SelectField + floatingLabelText="Sex" + errorText={touched && error} + {...input} + > + <MenuItem value="M" primaryText="Male" /> + <MenuItem value="F" primaryText="Female" /> + </SelectField> +); +const SexInput = ({ source }) => <Field name={source} component={renderSexInput} /> +export default SexInput; +``` + +For more details on how to use redux-form's `<Field>` component, please refer to [the redux-form doc](http://redux-form.com/6.5.0/docs/api/Field.md/). + +Instead of HTML `input` elements or material-ui components, you can use react-admin input components, like `<NumberInput>` for instance. React-admin components are already decorated by `<Field>`, and already include a label, so you don't need either `<Field>` or `<Labeled>` when using them: + +```jsx +// in LatLongInput.js +import { NumberInput } from 'react-admin'; +const LatLngInput = () => ( + <span> + <NumberInput source="lat" label="latitude" /> +   + <NumberInput source="lng" label="longitude" /> + </span> +); +export default LatLngInput; + +// in ItemEdit.js +const ItemEdit = (props) => ( + <Edit {...props}> + <SimpleForm> + <DisabledInput source="id" /> + <LatLngInput /> + </SimpleForm> + </Edit> +); +``` + +## Linking Two Inputs + +Edition forms often contain linked inputs, e.g. country and city (the choices of the latter depending on the value of the former). + +React-admin relies on redux-form, so you can grab the current form values using redux-form [formValueSelector()](https://redux-form.com/7.3.0/docs/api/formvalueselector.md/). Alternatively, you can use the react-admin `<FormDataConsumer>` component, which grabs the form values, and passes them to a child function. + +This facilitates the implementation of linked inputs: + +```jsx +import { FormDataConsumer } from 'react-admin'; + +const OrderEdit = (props) => ( + <Edit {...props}> + <SimpleForm> + <SelectInput source="country" choices={countries} /> + <FormDataConsumer> + {({ formData, ...rest }) => + <SelectInput + source="city" + choices={getCitiesFor(formData.country)} + {...rest} + /> + } + </FormDataConsumer> + </SimpleForm> + </Edit> +); +``` + +## Hiding Inputs Based On Other Inputs + +You may want to display or hide inputs base on the value of another input - for instance, show an `email` input only if the `hasEmail` boolean input is ticked to `true`. + +For such cases, you can use the approach described above, using the `<FormDataConsumer>` component. + +```jsx +import { FormDataConsumer } from 'react-admin'; + + const PostEdit = (props) => ( + <Edit {...props}> + <SimpleForm> + <BooleanInput source="hasEmail" /> + <FormDataConsumer> + {({ formData, ...rest }) => formData.hasEmail && + <TextInput source="email" {...rest} /> + } + </FormDataConsumer> + </SimpleForm> + </Edit> + ); +``` \ No newline at end of file From b28bb0866a95a0d632f46a2c4aaba453ec70c0dd Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:12 +0800 Subject: [PATCH 11/20] New translations admin-component.md (Chinese Simplified) --- .../translated_docs/zh-CN/admin-component.md | 556 ++++++++++++++++++ 1 file changed, 556 insertions(+) create mode 100644 website/translated_docs/zh-CN/admin-component.md diff --git a/website/translated_docs/zh-CN/admin-component.md b/website/translated_docs/zh-CN/admin-component.md new file mode 100644 index 0000000..cf05bd9 --- /dev/null +++ b/website/translated_docs/zh-CN/admin-component.md @@ -0,0 +1,556 @@ +--- +id: admin-component +title: <Admin> +--- +The `<Admin>` component creates an application with its own state, routing, and controller logic. `<Admin>` requires only a `dataProvider` prop, and at least one child `<Resource>` to work: + +```jsx +// in src/App.js +import React from 'react'; + +import { Admin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; + +import { PostList } from './posts'; + +const App = () => ( + <Admin dataProvider={simpleRestProvider('http://path.to.my.api')}> + <Resource name="posts" list={PostList} /> + </Admin> +); + +export default App; +``` + +Here are all the props accepted by the component: + +* [`dataProvider`](#dataprovider) +* [`title`](#title) +* [`dashboard`](#dashboard) +* [`catchAll`](#catchall) +* [`menu`](#menu) (deprecated) +* [`theme`](#theme) +* [`appLayout`](#applayout) +* [`customReducers`](#customreducers) +* [`customSagas`](#customsagas) +* [`customRoutes`](#customroutes) +* [`authProvider`](#authprovider) +* [`loginPage`](#loginpage) +* [`logoutButton`](#logoutbutton) +* [`locale`](#internationalization) +* [`messages`](#internationalization) +* [`initialState`](#initialstate) +* [`history`](#history) + +## `dataProvider` + +The only required prop, it must be a function returning a promise, with the following signature: + +```jsx +/** + * Query a data provider and return a promise for a response + * + * @example + * dataProvider(GET_ONE, 'posts', { id: 123 }) + * => new Promise(resolve => resolve({ id: 123, title: "hello, world" })) + * + * @param {string} type Request type, e.g GET_LIST + * @param {string} resource Resource name, e.g. "posts" + * @param {Object} payload Request parameters. Depends on the action type + * @returns {Promise} the Promise for a response + */ +const dataProvider = (type, resource, params) => new Promise(); +``` + +The `dataProvider` is also the ideal place to add custom HTTP headers, authentication, etc. The [Data Providers Chapter](./DataProviders.html) of the documentation lists available data providers, and explains how to build your own. + +## `title` + +By default, the header of an admin app uses 'React Admin' as the main app title. It's probably the first thing you'll want to customize. The `title` prop serves exactly that purpose. + +```jsx +const App = () => ( + <Admin title="My Custom Admin" dataProvider={simpleRestProvider('http://path.to.my.api')}> + // ... + </Admin> +); +``` + +## `dashboard` + +By default, the homepage of an an admin app is the `list` of the first child `<Resource>`. But you can also specify a custom component instead. To fit in the general design, use Material UI's `<Card>` component, and react-admin's `<ViewTitle>` component: + +```jsx +// in src/Dashboard.js +import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { ViewTitle } from 'react-admin'; +export default () => ( + <Card> + <ViewTitle title="Welcome to the administration" /> + <CardContent>Lorem ipsum sic dolor amet...</CardContent> + </Card> +); +``` + +```jsx +// in src/App.js +import Dashboard from './Dashboard'; + +const App = () => ( + <Admin dashboard={Dashboard} dataProvider={simpleRestProvider('http://path.to.my.api')}> + // ... + </Admin> +); +``` + +![Custom home page](https://marmelab.com/react-admin/img/dashboard.png) + +**Tip**: Adding the `<ViewTitle>` component will also allow the header to be displayed in mobile resolutions. + +## `catchAll` + +When users type URLs that don't match any of the children `<Resource>` components, they see a default "Not Found" page. + +![Not Found](https://marmelab.com/react-admin/img/not-found.png) + +You can customize this page to use the component of your choice by passing it as the `catchAll` prop. To fit in the general design, use Material UI's `<Card>` component, and react-admin's `<ViewTitle>` component: + +```jsx +// in src/NotFound.js +import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { ViewTitle } from 'react-admin'; + +export default () => ( + <Card> + <ViewTitle title="Not Found" /> + <CardContent> + <h1>404: Page not found</h1> + </CardContent> + </Card> +); +``` + +```jsx +// in src/App.js +import NotFound from './NotFound'; + +const App = () => ( + <Admin catchAll={NotFound} dataProvider={simpleRestProvider('http://path.to.my.api')}> + // ... + </Admin> +); +``` + +**Tip**: If your custom `catchAll` component contains react-router `<Route>` components, this allows you to register new routes displayed within the react-admin layout easily. Note that these routes will match *after* all the react-admin resource routes have been tested. To add custom routes *before* the react-admin ones, and therefore override the default resource routes, use the [`customRoutes` prop](#customroutes) instead. + +## `menu` + +**Tip**: This prop is deprecated. To override the menu component, use a [custom layout](#appLayout) instead. + +React-admin uses the list of `<Resource>` components passed as children of `<Admin>` to build a menu to each resource with a `list` component. + +If you want to add or remove menu items, for instance to link to non-resources pages, you can create your own menu component: + +```jsx +// in src/Menu.js +import React, { createElement } from 'react'; +import { connect } from 'react-redux'; +import { MenuItemLink, getResources } from 'react-admin'; +import { withRouter } from 'react-router-dom'; +import LabelIcon from '@material-ui/icons/Label'; + +import Responsive from '../layout/Responsive'; + +const Menu = ({ resources, onMenuClick, logout }) => ( + <div> + {resources.map(resource => ( + <MenuItemLink + to={`/${resource.name}`} + primaryText={resource.name} + leftIcon={createElement(resource.icon)} + onClick={onMenuClick} + /> + ))} + <MenuItemLink + to="/custom-route" + primaryText="Miscellaneous" + leftIcon={<LabelIcon />} + onClick={onMenuClick} /> + <Responsive + small={logout} + medium={null} // Pass null to render nothing on larger devices + /> + </div> +); + +const mapStateToProps = state => ({ + resources: getResources(state), +}); + +export default withRouter(connect(mapStateToProps)(Menu)); +``` + +**Tip**: Note the `MenuItemLink` component. It must be used to avoid unwanted side effects in mobile views. It supports a custom text and icon (which must be a material-ui `<SvgIcon>`). + +**Tip**: Note that we include the `logout` item only on small devices. Indeed, the `logout` button is already displayed in the AppBar on larger devices. + +**Tip**: Note that we use React Router [`withRouter`](https://reacttraining.com/react-router/web/api/withRouter) Higher Order Component and that it is used **before** Redux [`connect](https://github.com/reactjs/react-redux/blob/master/docs/api.html#connectmapstatetoprops-mapdispatchtoprops-mergeprops-options). This is required if you want the active menu item to be highlighted. + +Then, pass it to the `<Admin>` component as the `menu` prop: + +```jsx +// in src/App.js +import Menu from './Menu'; + +const App = () => ( + <Admin menu={Menu} dataProvider={simpleRestProvider('http://path.to.my.api')}> + // ... + </Admin> +); +``` + +See the [Theming documentation](./Theming.html#using-a-custom-menu) for more details. + +## `theme` + +Material UI supports [theming](http://www.material-ui.com/#/customization/themes). This lets you customize the look and feel of an admin by overriding fonts, colors, and spacing. You can provide a custom material ui theme by using the `theme` prop: + +```jsx +import { createMuiTheme } from '@material-ui/core/styles'; + +const theme = createMuiTheme({ + palette: { + type: 'dark', // Switching the dark mode on is a single property value change. + }, +}); + +const App = () => ( + <Admin theme={theme} dataProvider={simpleRestProvider('http://path.to.my.api')}> + // ... + </Admin> +); +``` + +![Dark theme](https://marmelab.com/react-admin/img/dark-theme.png) + +For more details on predefined themes and custom themes, refer to the [Material UI Customization documentation](https://material-ui.com/customization/themes/). + +## `appLayout` + +If you want to deeply customize the app header, the menu, or the notifications, the best way is to provide a custom layout component. It must contain a `{children}` placeholder, where react-admin will render the resources. If you use material UI fields and inputs, it should contain a `<MuiThemeProvider>` element. And finally, if you want to show the spinner in the app header when the app fetches data in the background, the Layout should connect to the redux store. + +Use the [default layout](https://github.com/marmelab/react-admin/blob/master/packages/ra-ui-materialui/src/layout/Layout.js) as a starting point, and check [the Theming documentation](./Theming.html#using-a-custom-layout) for examples. + +```jsx +// in src/App.js +import MyLayout from './MyLayout'; + +const App = () => ( + <Admin appLayout={MyLayout} dataProvider={simpleRestProvider('http://path.to.my.api')}> + // ... + </Admin> +); +``` + +Your custom layout can simply extend the default `<Layout>` component if you only want to override the appBar, the menu, or the notification component. For instance: + +```jsx +// in src/MyLayout.js +import { Layout } from 'react-admin'; +import MyAppBar from './MyAppBar'; +import MyMenu from './MyMenu'; +import MyNotification from './MyNotification'; + +const MyLayout = (props) => <Layout + {...props} + appBar={MyAppBar} + menu={MyMenu} + notification={MyNotification} +/>; + +export default MyLayout; +``` + +For more details on custom layouts, check [the Theming documentation](./Theming.html#using-a-custom-layout). + +## `customReducers` + +The `<Admin>` app uses [Redux](http://redux.js.org/) to manage state. The state has the following keys: + +```jsx +{ + admin: { /*...*/ }, // used by react-admin + form: { /*...*/ }, // used by redux-form + routing: { /*...*/ }, // used by react-router-redux +} +``` + +If your components dispatch custom actions, you probably need to register your own reducers to update the state with these actions. Let's imagine that you want to keep the bitcoin exchange rate inside the `bitcoinRate` key in the state. You probably have a reducer looking like the following: + +```jsx +// in src/bitcoinRateReducer.js +export default (previousState = 0, { type, payload }) => { + if (type === 'BITCOIN_RATE_RECEIVED') { + return payload.rate; + } + return previousState; +} +``` + +To register this reducer in the `<Admin>` app, simply pass it in the `customReducers` prop: + +{% raw %} + +```jsx +// in src/App.js +import React from 'react'; +import { Admin } from 'react-admin'; + +import bitcoinRateReducer from './bitcoinRateReducer'; + +const App = () => ( + <Admin customReducers={{ bitcoinRate: bitcoinRateReducer }} dataProvider={simpleRestProvider('http://path.to.my.api')}> + ... + </Admin> +); + +export default App; +``` + +{% endraw %} + +Now the state will look like: + +```jsx +{ + admin: { /*...*/ }, // used by react-admin + form: { /*...*/ }, // used by redux-form + routing: { /*...*/ }, // used by react-router-redux + bitcoinRate: 123, // managed by rateReducer +} +``` + +## `customSagas` + +The `<Admin>` app uses [redux-saga](https://github.com/redux-saga/redux-saga) to handle side effects (AJAX calls, notifications, redirections, etc). + +If your components dispatch custom actions, you probably need to register your own side effects as sagas. Let's imagine that you want to show a notification whenever the `BITCOIN_RATE_RECEIVED` action is dispatched. You probably have a saga looking like the following: + +```jsx +// in src/bitcoinSaga.js +import { put, takeEvery } from 'redux-saga/effects'; +import { showNotification } from 'react-admin'; + +export default function* bitcoinSaga() { + yield takeEvery('BITCOIN_RATE_RECEIVED', function* () { + yield put(showNotification('Bitcoin rate updated')); + }) +} +``` + +To register this saga in the `<Admin>` app, simply pass it in the `customSagas` prop: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin } from 'react-admin'; + +import bitcoinSaga from './bitcoinSaga'; + +const App = () => ( + <Admin customSagas={[ bitcoinSaga ]} dataProvider={simpleRestProvider('http://path.to.my.api')}> + ... + </Admin> +); + +export default App; +``` + +## `customRoutes` + +To register your own routes, create a module returning a list of [react-router](https://github.com/ReactTraining/react-router) `<Route>` component: + +```jsx +// in src/customRoutes.js +import React from 'react'; +import { Route } from 'react-router-dom'; +import Foo from './Foo'; +import Bar from './Bar'; +import Baz from './Baz'; + +export default [ + <Route exact path="/foo" component={Foo} />, + <Route exact path="/bar" component={Bar} />, + <Route exact path="/baz" component={Baz} noLayout />, +]; +``` + +Then, pass this array as `customRoutes` prop in the `<Admin>` component: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin } from 'react-admin'; + +import customRoutes from './customRoutes'; + +const App = () => ( + <Admin customRoutes={customRoutes} dataProvider={simpleRestProvider('http://path.to.my.api')}> + ... + </Admin> +); + +export default App; +``` + +Now, when a user browses to `/foo` or `/bar`, the components you defined will appear in the main part of the screen. When a user browses to `/baz`, the component will appear outside of the defined Layout, leaving you the freedom to design the screen the way you want. + +**Tip**: It's up to you to create a [custom menu](#menu) entry, or custom buttons, to lead to your custom pages. + +**Tip**: Your custom pages take precedence over react-admin's own routes. That means that `customRoutes` lets you override any route you want! If you want to add routes *after* all the react-admin routes, use the [`catchAll` prop](#catchall) instead. + +**Tip**: To look like other react-admin pages, your custom pages should have the following structure: + +```jsx +// in src/Foo.js +import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { ViewTitle } from 'react-admin'; + +const Foo = () => ( + <Card> + <ViewTitle title="My Page" /> + <CardContent> + ... + </CardContent> + </Card> +)); + +export default Foo; +``` + +## `authProvider` + +The `authProvider` prop expect a function returning a Promise, to control the application authentication strategy: + +```jsx +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin'; + +const authProvider(type, params) { + // type can be any of AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, and AUTH_CHECK + // ... + return Promise.resolve(); +}; + +const App = () => ( + <Admin authProvider={authProvider} dataProvider={simpleRestProvider('http://path.to.my.api')}> + ... + </Admin> +); +``` + +The [Authentication documentation](./Authentication.html) explains how to implement these functions in detail. + +## `loginPage` + +If you want to customize the Login page, or switch to another authentication strategy than a username/password form, pass a component of your own as the `loginPage` prop. React-admin will display this component whenever the `/login` route is called. + +```jsx +import MyLoginPage from './MyLoginPage'; + +const App = () => ( + <Admin loginPage={MyLoginPage}> + ... + </Admin> +); +``` + +See The [Authentication documentation](./Authentication.html#customizing-the-login-and-logout-components) for more details. + +## `logoutButton` + +If you customize the `loginPage`, you probably need to override the `logoutButton`, too - because they share the authentication strategy. + +```jsx +import MyLoginPage from './MyLoginPage'; +import MyLogoutButton from './MyLogoutButton'; + +const App = () => ( + <Admin loginPage={MyLoginPage} logoutButton={MyLogoutButton}> + ... + </Admin> +); +``` + +## `initialState` + +The `initialState` prop lets you pass preloaded state to Redux. See the [Redux Documentation](http://redux.js.org/docs/api/createStore.html#createstorereducer-preloadedstate-enhancer) for more details. + +## `history` + +By default, react-admin creates URLs using a hash sign (e.g. "myadmin.acme.com/#/posts/123"). The hash portion of the URL (i.e. `#/posts/123` in the example) contains the main application route. This strategy has the benefit of working without a server, and with legacy web browsers. But you may want to use another routing strategy, e.g. to allow server-side rendering. + +You can create your own `history` function (compatible with [the `history` npm package](https://github.com/reacttraining/history)), and pass it to the `<Admin>` component to override the default history strategy. For instance, to use `browserHistory`: + +```js +import createHistory from 'history/createBrowserHistory'; + +const history = createHistory(); + +const App = () => ( + <Admin history={history}> + ... + </Admin> +); +``` + +## Internationalization + +The `locale` and `messages` props let you translate the GUI. The [Translation Documentation](./Translation.html) details this process. + +## Declaring resources at runtime + +You might want to dynamically define the resources when the app starts. The `<Admin>` component accepts a function as its child and this function can return a Promise. If you also defined an `authProvider`, the function will receive the result of a call to `authProvider` with the `AUTH_GET_PERMISSIONS` type (you can read more about this in the [Authorization](./Authorization.html) chapter). + +For instance, getting the resource from an API might look like: + +```js +import React from 'react'; + +import { Admin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; + +import { PostList } from './posts'; +import { CommentList } from './comments'; + +const knownResources = [ + <Resource name="posts" list={PostList} />, + <Resource name="comments" list={CommentList} />, +]; + +const fetchResources = permissions => + fetch('https://myapi/resources', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(permissions), + }) + .then(response => response.json()) + .then(json => knownResources.filter(resource => json.resources.includes(resource.props.name))); + +const App = () => ( + <Admin dataProvider={simpleRestProvider('http://path.to.my.api')}> + {fetchResources} + </Admin> +); +``` + +## Using react-admin without `<Admin>` and `<Resource>` + +Using `<Admin>` and `<Resource>` is completely optional. If you feel like bootstrapping a redux app yourself, it's totally possible. Head to [Including in another app](./CustomApp.html) for a detailed how-to. \ No newline at end of file From 92b7bdfdc68cf9e8e452432a4db5814bcc764c43 Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:13 +0800 Subject: [PATCH 12/20] New translations field-components.md (Chinese Simplified) --- .../translated_docs/zh-CN/field-components.md | 1003 +++++++++++++++++ 1 file changed, 1003 insertions(+) create mode 100644 website/translated_docs/zh-CN/field-components.md diff --git a/website/translated_docs/zh-CN/field-components.md b/website/translated_docs/zh-CN/field-components.md new file mode 100644 index 0000000..5699834 --- /dev/null +++ b/website/translated_docs/zh-CN/field-components.md @@ -0,0 +1,1003 @@ +--- +layout: field-components +title: <Field> Components +--- +A `Field` component displays a given property of a REST resource. Such components are used in the `List` view, but you can also use them in the `Edit` and `Create` views for read-only fields. The most usual of all field components is `<TextField>`: + +```jsx +// in src/posts.js +import React from 'react'; +import { List, Datagrid, TextField } from 'react-admin'; + +export const PostList = (props) => ( + <List {...props}> + <Datagrid> + <TextField source="id" /> + <TextField source="title" /> + <TextField source="body" /> + </Datagrid> + </List> +); +``` + +All field components accept the following attributes: + +- `source`: Property name of your entity to view/edit. This attribute is required. +- `label`: Used as a table header of an input label. Defaults to the `source` when omitted. +- `sortable`: Should the list be sortable using `source` attribute? Defaults to `true`. +- `className`: A class name (usually generated by JSS) to customize the look and feel of the field element itself +- `cellClassName`: A class name (usually generated by JSS) to customize the look and feel of the field container (e.g. the `<td>` in a datagrid). +- `headerClassName`: A class name (usually generated by JSS) to customize the look and feel of the field header (e.g. the `<th>` in a datagrid). +- `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`. + +```jsx +<TextField source="zb_title" label="Title" style={{ color: 'purple' }} /> +``` + +**Tip**: You can use field components inside the `Edit` or `Show` views, too: + +```jsx +export const PostShow = ({ ...props }) => ( + <Show {...props}> + <SimpleShowLayout> + <TextField source="title" /> + </SimpleShowLayout> + </Show> +); +``` + +**Tip**: If you display a record with a complex structure, you can use a path with dot separators as the `source` attribute. For instance, if the API returns the following 'book' record: + +```jsx +{ + id: 1234, + title: 'War and Peace', + author: { + firstName: 'Leo', + lastName: 'Tolstoi' + } +} +``` + +Then you can display the author first name as follows: + +```jsx +<TextField source="author.firstName" /> +``` + +**Tip**: If you want to format a field according to the value, use a higher-order component to do conditional formatting, as described in the [Theming documentation](./Theming.md#conditional-formatting). + +**Tip**: If your interface has to support multiple languages, don't use the `label` prop, and put the localized labels in a dictionary instead. See the [Translation documentation](./Translation.md#translating-resource-and-field-names) for details. + +## `ArrayField` Component + +Display a collection using `<Field>` child components. + +Ideal for embedded arrays of objects, e.g. `tags` and `backlinks` in the following `post` object: + +```js +{ + id: 123 + tags: [ + { name: 'foo' }, + { name: 'bar' } + ], + backlinks: [ + { + date: '2012-08-10T00:00:00.000Z', + url: 'http://example.com/foo/bar.html', + }, + { + date: '2012-08-14T00:00:00.000Z', + url: 'https://blog.johndoe.com/2012/08/12/foobar.html', + } + ] +} +``` + +The child must be an iterator component (like `<Datagrid>` or `<SingleFieldList>`). + +Here is how to display all the backlinks of the current post as a `<datagrid>` + +```jsx +<ArrayField source="backlinks"> + <Datagrid> + <DateField source="date" /> + <UrlField source="url" /> + </Datagrid> +</ArrayField> +``` + +And here is how to display all the tags of the current post as `<Chip>` components: + +```jsx +<ArrayField source="tags"> + <SingleFieldList> + <ChipField source="name" /> + </SingleFieldList> +</ArrayField> +``` + +**Tip**: If you need to render a collection in a custom way, it's often simpler to write your own component: + +```jsx +const TagsField = ({ record }) => ( + <ul> + {record.tags.map(item => ( + <li key={item.name}>{item.name}</li> + ))} + </ul> +) +TagsField.defaultProps = { addLabel: true }; +``` + +## `BooleanField` Component + +Displays a boolean value as a check. + +```jsx +import { BooleanField } from 'react-admin'; + +<BooleanField source="commentable" /> +``` + +![BooleanField](https://marmelab.com/react-admin/img/boolean-field.png) + +## `ChipField` Component + +Displays a value inside a ["Chip"](http://www.material-ui.com/#/components/chip), which is Material UI's term for a label. + +```jsx +import { ChipField } from 'react-admin'; + +<ChipField source="category" /> +``` + +![ChipField](https://marmelab.com/react-admin/img/chip-field.png) + +This field type is especially useful for one to many relationships, e.g. to display a list of books for a given author: + +```jsx +import { ChipField, SingleFieldList, ReferenceManyField } from 'react-admin'; + +<ReferenceManyField reference="books" target="author_id"> + <SingleFieldList> + <ChipField source="title" /> + </SingleFieldList> +</ReferenceManyField> +``` + +## `DateField` Component + +Displays a date or datetime using the browser locale (thanks to `Date.toLocaleDateString()` and `Date.toLocaleString()`). + +```jsx +import { DateField } from 'react-admin'; + +<DateField source="publication_date" /> +``` + +This component accepts a `showTime` attribute (false by default) to force the display of time in addition to date. It uses `Intl.DateTimeFormat()` if available, passing the `locales` and `options` props as arguments. If Intl is not available, it ignores the `locales` and `options` props. + +```jsx +<DateField source="publication_date" /> +// renders the record { id: 1234, publication_date: new Date('2017-04-23') } as +<span>4/23/2017</span> + +<DateField source="publication_date" showTime /> +// renders the record { id: 1234, publication_date: new Date('2017-04-23 23:05') } as +<span>4/23/2017, 11:05:00 PM</span> + +<DateField source="publication_date" options={{ weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }} /> +// renders the record { id: 1234, publication_date: new Date('2017-04-23') } as +<span>Sunday, April 23, 2017</span> + +<DateField source="publication_date" locales="fr-FR" /> +// renders the record { id: 1234, publication_date: new Date('2017-04-23') } as +<span>23/04/2017</span> + +<DateField source="publication_date" elStyle={{ color: 'red' }} /> +// renders the record { id: 1234, publication_date: new Date('2017-04-23') } as +<span style="color:red;">4/23/2017</span> +``` + +See [Intl.DateTimeformat documentation](https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Objets_globaux/Date/toLocaleDateString) for the `options` prop syntax. + +**Tip**: If you need more formatting options than what `Intl.DateTimeformat` can provide, build your own field component leveraging a third-party library like [moment.js](http://momentjs.com/). + +## `EmailField` Component + +`<EmailField>` displays an email as a `<a href="mailto:" />` link. + +```jsx +import { EmailField } from 'react-admin'; + +<EmailField source="personal_email" /> +``` + +## `FunctionField` Component + +If you need a special function to render a field, `<FunctionField>` is the perfect match. It passes the `record` to a `render` function supplied by the developer. For instance, to display the full name of a `user` record based on `first_name` and `last_name` properties: + +```jsx +import { FunctionField } from 'react-admin' + +<FunctionField label="Name" render={record => `${record.first_name} ${record.last_name}`} /> +``` + +**Tip**: Technically, you can omit the `source` and `sortBy` properties for the `<FunctionField>` since you provide the render function. However, providing a `source` or a `sortBy` will allow the datagrid to make the column sortable, since when a user clicks on a column, the datagrid uses these properties to sort. Should you provide both, `sortBy` will override `source` for sorting the column. + +## `ImageField` Component + +If you need to display an image provided by your API, you can use the `<ImageField />` component: + +```jsx +import { ImageField } from 'react-admin'; + +<ImageField source="url" title="title" /> +``` + +This field is also generally used within an [<imageinput />](http://marmelab.com/react-admin/Inputs.md#imageinput) component to display preview. + +The optional `title` prop points to the picture title property, used for both `alt` and `title` attributes. It can either be an hard-written string, or a path within your JSON object: + +```jsx +// { picture: { url: 'cover.jpg', title: 'Larry Cover (French pun intended)' } } + +// Title would be "picture.title", hence "Larry Cover (French pun intended)" +<ImageField source="picture.url" title="picture.title" /> + +// Title would be "Picture", as "Picture" is not a path in previous given object +<ImageField source="picture.url" title="Picture" /> +``` + +If passed value is an existing path within your JSON object, then it uses the object attribute. Otherwise, it considers its value as an hard-written title. + +If the record actually contains an array of images in its property defined by the `source` prop, the `src` prop will be needed to determine the `src` value of the images, for example: + +```js +// This is the record +{ + pictures: [ + { url: 'image1.jpg', desc: 'First image' }, + { url: 'image2.jpg', desc: 'Second image' }, + ], +} + +<ImageField source="pictures" src="url" title="desc" /> +``` + +## `FileField` Component + +If you need to display a file provided by your API, you can use the `<FileField />` component: + +```jsx +import { FileField } from 'react-admin'; + +<FileField source="url" title="title" /> +``` + +This field is also generally used within an [<fileinput />](http://marmelab.com/react-admin/Inputs.md#fileinput) component to display preview. + +The optional `title` prop points to the file title property, used for `title` attributes. It can either be an hard-written string, or a path within your JSON object: + +```jsx +// { file: { url: 'doc.pdf', title: 'Presentation' } } + +// Title would be "file.title", hence "Presentation" +<FileField source="file.url" title="file.title" /> + +// Title would be "File", as "File" is not a path in previous given object +<FileField source="file.url" title="File" /> +``` + +If passed value is an existing path within your JSON object, then it uses the object attribute. Otherwise, it considers its value as an hard-written title. + +If the record actually contains an array of files in its property defined by the `source` prop, the `src` prop will be needed to determine the `href` value of the links, for example: + +```js +// This is the record +{ + files: [ + { url: 'image1.jpg', desc: 'First image' }, + { url: 'image2.jpg', desc: 'Second image' }, + ], +} + +<FileField source="files" src="url" title="desc" /> +``` + +You can optionally set the `target` prop to choose which window will the link try to open in. + +```jsx +// Will make the file open in new window +<FileField source="file.url" target="_blank" /> +``` + +## `NumberField` Component + +Displays a number formatted according to the browser locale, right aligned. + +Uses `Intl.NumberFormat()` if available, passing the `locales` and `options` props as arguments. This allows perfect display of decimals, currencies, percentage, etc. + +If Intl is not available, it outputs number as is (and ignores the `locales` and `options` props). + +```jsx +import { NumberField } from 'react-admin'; + +<NumberField source="score" /> +// renders the record { id: 1234, score: 567 } as +<span>567</span> + +<NumberField source="score" options={{ maximumFractionDigits: 2 }}/> +// renders the record { id: 1234, score: 567.3567458569 } as +<span>567.35</span> + +<NumberField source="share" options={{ style: 'percent' }} /> +// renders the record { id: 1234, share: 0.2545 } as +<span>25%</span> + +<NumberField source="price" options={{ style: 'currency', currency: 'USD' }} /> +// renders the record { id: 1234, price: 25.99 } as +<span>$25.99</span> + +<NumberField source="price" locales="fr-FR" options={{ style: 'currency', currency: 'USD' }} /> +// renders the record { id: 1234, price: 25.99 } as +<span>25,99 $US</span> + +<NumberField source="score" elStyle={{ color: 'red' }} /> +// renders the record { id: 1234, score: 567 } as +<span style="color:red;">567</span> +``` + +See [Intl.Numberformat documentation](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString) for the `options` prop syntax. + +**Tip**: If you need more formatting options than what `Intl.Numberformat` can provide, build your own field component leveraging a third-party library like [numeral.js](http://numeraljs.com/). + +**Tip**: When used in a `Show` view, the right alignment may look weird. Disable it by resetting the `style` attribute: + +```jsx +import { NumberField } from 'react-admin'; + +<NumberField source="score" style={{}} /> +``` + +## `SelectField` Component + +When you need to display an enumerated field, `<SelectField>` maps the value to a string. + +For instance, if the `gender` field can take values "M" and "F", here is how to display it as "Male" or "Female": + +```jsx +import { SelectField } from 'react-admin'; + +<SelectField source="gender" choices={[ + { id: 'M', name: 'Male' }, + { id: 'F', name: 'Female' }, +]} /> +``` + +By default, the text is built by + +- finding a choice where the 'id' property equals the field value +- using the 'name' property an the option text + +**Warning**: This component name may conflict with material-ui's [`<SelectField>`](http://www.material-ui.com/#/components/select-field) if you import both. + +You can also customize the properties to use for the lookup value and text, thanks to the 'optionValue' and 'optionText' attributes. + +```jsx +const choices = [ + { _id: 123, full_name: 'Leo Tolstoi', sex: 'M' }, + { _id: 456, full_name: 'Jane Austen', sex: 'F' }, +]; +<SelectField source="author_id" choices={choices} optionText="full_name" optionValue="_id" /> +``` + +`optionText` also accepts a function, so you can shape the option text at will: + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const optionRenderer = choice => `${choice.first_name} ${choice.last_name}`; +<SelectField source="author_id" choices={choices} optionText={optionRenderer} /> +``` + +`optionText` also accepts a React Element, that will be cloned and receive the related choice as the `record` prop. You can use Field components there. + +```jsx +const choices = [ + { id: 123, first_name: 'Leo', last_name: 'Tolstoi' }, + { id: 456, first_name: 'Jane', last_name: 'Austen' }, +]; +const FullNameField = ({ record }) => <Chip>{record.first_name} {record.last_name}</Chip>; +<SelectField source="gender" choices={choices} optionText={<FullNameField />}/> +``` + +The current choice is translated by default, so you can use translation identifiers as choices: + +```jsx +const choices = [ + { id: 'M', name: 'myroot.gender.male' }, + { id: 'F', name: 'myroot.gender.female' }, +]; +``` + +However, in some cases (e.g. inside a `<ReferenceField>`), you may not want the choice to be translated. In that case, set the `translateChoice` prop to false. + +```jsx +<SelectField source="gender" choices={choices} translateChoice={false}/> +``` + +**Tip**: <referencefield> sets `translateChoice` to false by default. + +## `ReferenceField` Component + +This component fetches a single referenced record (using the `GET_MANY` REST method), and displays one field of this record. That's why a `<ReferenceField>` must always have a child `<Field>`. + +For instance, here is how to fetch the `post` related to `comment` records, and display the `title` for each: + +```jsx +import React from 'react'; +import { List, Datagrid, ReferenceField, TextField } from 'react-admin'; + +export const PostList = (props) => ( + <List {...props}> + <Datagrid> + <TextField source="id" /> + <ReferenceField label="Author" source="user_id" reference="users"> + <TextField source="name" /> + </ReferenceField> + </Datagrid> + </List> +); +``` + +With this configuration, `<ReferenceField>` wraps the user's name in a link to the related user `<Edit>` page. + +![ReferenceField](https://marmelab.com/react-admin/img/reference-field.png) + +`<ReferenceField>` accepts a `reference` attribute, which specifies the resource to fetch for the related record. Also, you can use any `Field` component as child. + +**Note**: You **must** add a `<Resource>` for the reference resource - react-admin needs it to fetch the reference data. You *can* omit the `list` prop in this reference if you want to hide it in the sidebar menu. + +```jsx +<Admin dataProvider={myDataProvider}> + <Resource name="comments" list={CommentList} /> + <Resource name="posts" /> +</Admin> +``` + +To change the link from the `<Edit>` page to the `<Show>` page, set the `linkType` prop to "show". + +```jsx +<ReferenceField label="User" source="userId" reference="users" linkType="show"> + <TextField source="name" /> +</ReferenceField> +``` + +By default, `<ReferenceField>` is sorted by its `source`. To specify another attribute to sort by, set the `sortBy` prop to the according attribute's name. + +```jsx +<ReferenceField label="User" source="userId" reference="users" sortBy="user.name"> + <TextField source="name" /> +</ReferenceField> +``` + +You can also prevent `<ReferenceField>` from adding link to children by setting `linkType` to `false`. + +```jsx +// No link +<ReferenceField label="User" source="userId" reference="users" linkType={false}> + <TextField source="name" /> +</ReferenceField> +``` + +**Tip**: React-admin uses `CRUD_GET_ONE_REFERENCE` action to accumulate and deduplicate the ids of the referenced records to make *one* `GET_MANY` call for the entire list, instead of n `GET_ONE` calls. So for instance, if the API returns the following list of comments: + +```jsx +[ + { + id: 123, + body: 'Totally agree', + post_id: 789, + }, + { + id: 124, + title: 'You are right my friend', + post_id: 789 + }, + { + id: 125, + title: 'Not sure about this one', + post_id: 735 + } +] +``` + +Then react-admin renders the `<CommentList>` with a loader for the `<ReferenceField>`, fetches the API for the related posts in one call (`GET http://path.to.my.api/posts?ids=[789,735]`), and re-renders the list once the data arrives. This accelerates the rendering, and minimizes network load. + +## `ReferenceManyField` Component + +This component fetches a list of referenced records by reverse lookup of the current `record.id` in other resource (using the `GET_MANY_REFERENCE` REST method). The field name of the current record's id in the other resource is specified by the required `target` field. The result is then passed to an iterator component (like `<SingleFieldList>` or `<Datagrid>`). The iterator component usually has one or more child `<Field>` components. + +For instance, here is how to fetch the `comments` related to a `post` record by matching `comment.post_id` to `post.id`, and then display the `author.name` for each, in a `<ChipField>`: + +```jsx +import React from 'react'; +import { List, Datagrid, ChipField, ReferenceManyField, SingleFieldList, TextField } from 'react-admin'; + +export const PostList = (props) => ( + <List {...props}> + <Datagrid> + <TextField source="id" /> + <TextField source="title" type="email" /> + <ReferenceManyField label="Comments by" reference="comments" target="post_id"> + <SingleFieldList> + <ChipField source="author.name" /> + </SingleFieldList> + </ReferenceManyField> + <EditButton /> + </Datagrid> + </List> +); +``` + +![ReferenceManyFieldSingleFieldList](https://marmelab.com/react-admin/img/reference-many-field-single-field-list.png) + +`<ReferenceManyField>` accepts a `reference` attribute, which specifies the resource to fetch for the related record. It also accepts a `source` attribute which define the field containing the value to look for in the `target` field of the referenced resource. By default this is the `id` of the resource (`post.id` in the previous example). + +**Note**: You **must** add a `<Resource>` for the reference resource - react-admin needs it to fetch the reference data. You *can* omit the `list` prop in this reference if you want to hide it in the sidebar menu. + +You can use a `<Datagrid>` instead of a `<SingleFieldList>` - but not inside another `<Datagrid>`! This is useful if you want to display a read-only list of related records. For instance, if you want to show the `comments` related to a `post` in the post's `<Edit>` view: + +```jsx +import React from 'react'; +import { Edit, Datagrid, SimpleForm, DisabledInput, DateField, EditButton, ReferenceManyField, TextField, TextInput } from 'react-admin'; + +export const PostEdit = (props) => ( + <Edit {...props}> + <SimpleForm> + <DisabledInput label="Id" source="id" /> + <TextInput source="title" /> + <ReferenceManyField + label="Comments" + reference="comments" + target="post_id" + > + <Datagrid> + <DateField source="created_at" /> + <TextField source="author.name" /> + <TextField source="body" /> + <EditButton /> + </Datagrid> + </ReferenceManyField> + </SimpleForm> + </Edit> +); +``` + +![ReferenceManyFieldDatagrid](https://marmelab.com/react-admin/img/reference-many-field-datagrid.png) + +By default, react-admin restricts the possible values to 25. You can change this limit by setting the `perPage` prop. + +```jsx +<ReferenceManyField perPage={10} reference="comments" target="post_id"> + ... +</ReferenceManyField> +``` + +By default, it orders the possible values by id desc. You can change this order by setting the `sort` prop (an object with `field` and `order` properties). + +```jsx +<ReferenceManyField sort={{ field: 'created_at', order: 'DESC' }} reference="comments" target="post_id"> + ... +</ReferenceManyField> +``` + +Also, you can filter the query used to populate the possible values. Use the `filter` prop for that. + +```jsx +<ReferenceManyField filter={{ is_published: true }} reference="comments" target="post_id"> + ... +</ReferenceManyField> +``` + +## `ReferenceArrayField` Component + +Use `<ReferenceArrayField>` to display an list of reference values based on an array of foreign keys. + +For instance, if a post has many tags, a post resource may look like: + +```js +{ + id: 1234, + title: 'Lorem Ipsum', + tag_ids: [1, 23, 4] +} +``` + +Where `[1, 23, 4]` refer to ids of `tag` resources. + +`<ReferenceArrayField>` can fetch the `tag` resources related to this `post` resource by matching `post.tag_ids` to `tag.id`. `<ReferenceArrayField source="tags_ids" reference="tags">` would issue an HTTP request looking like: + + http://myapi.com/tags?id=[1,23,4] + + +**Tip**: `<ReferenceArrayField>` fetches the related resources using the `GET_MANY` REST method, so the actual HTTP request depends on your REST client. + +Once it receives the related resources, `<ReferenceArrayField>` passes them to its child component using the `ids` and `data` props, so the child must be an iterator component (like `<SingleFieldList>` or `<Datagrid>`). The iterator component usually has one or more child `<Field>` components. + +Here is how to fetch the list of tags for each post in a `PostList`, and display the `name` for each `tag` in a `<ChipField>`: + +```jsx +import React from 'react'; +import { List, Datagrid, ChipField, ReferenceArrayField, SingleFieldList, TextField } from 'react-admin'; + +export const PostList = (props) => ( + <List {...props}> + <Datagrid> + <TextField source="id" /> + <TextField source="title" /> + <ReferenceArrayField label="Tags" reference="tags" source="tag_ids"> + <SingleFieldList> + <ChipField source="name" /> + </SingleFieldList> + </ReferenceArrayField> + <EditButton /> + </Datagrid> + </List> +); +``` + +**Note**: You **must** add a `<Resource>` component for the reference resource to your `<Admin>` component, because react-admin needs it to fetch the reference data. You can omit the `list` prop in this Resource if you don't want to show an entry for it in the sidebar menu. + +```jsx +export const App = () => ( + <Admin dataProvider={restProvider('http://path.to.my.api')}> + <Resource name="posts" list={PostList} /> + <Resource name="tags" /> // <= this one is compulsory + </Admin> +); +``` + +In an Edit of Show view, you can combine `<ReferenceArrayField>` with `<Datagrid>` to display a related resources in a table. For instance, to display more details about the tags related to a post in the `PostShow` view: + +```jsx +import React from 'react'; +import { Show, SimpleShowLayout, TextField, ReferenceArrayField, Datagrid, ShowButton } from 'react-admin'; + +export const PostShow = (props) => ( + <Show {...props}> + <SimpleShowLayout> + <TextField source="id" /> + <TextField source="title" /> + <ReferenceArrayField label="Tags" reference="tags" source="tag_ids"> + <Datagrid> + <TextField source="id" /> + <TextField source="name" /> + <ShowButton /> + </Datagrid> + </ReferenceArrayField> + <EditButton /> + </SimpleShowLayout> + </Show> +); +``` + +## `RichTextField` Component + +This component displays some HTML content. The content is "rich" (i.e. unescaped) by default. + +```jsx +import { RichTextField } from 'react-admin'; + +<RichTextField source="body" /> +``` + +![RichTextField](https://marmelab.com/react-admin/img/rich-text-field.png) + +The `stripTags` attribute (`false` by default) allows you to remove any HTML markup, preventing some display glitches (which is especially useful in list views). + +```jsx +import { RichTextField } from 'react-admin'; + +<RichTextField source="body" stripTags /> +``` + +## `TextField` Component + +The most simple as all fields, `<TextField>` simply displays the record property as plain text. + +```jsx +import { TextField } from 'react-admin'; + +<TextField label="Author Name" source="name" /> +``` + +## `UrlField` Component + +`<UrlField>` displays an url in an `< a href="">` tag. + +```jsx +import { UrlField } from 'react-admin'; + +<UrlField source="site_url" /> +``` + +## Styling Fields + +All field components accept a `className` prop, allowing you to customize their style to your liking. We advise you to use the Material UI styling solution, JSS, to generate those classes. See their [documentation](https://material-ui.com/customization/css-in-js/#api) about that. + +```jsx +import { withStyles } from '@material-ui/core/styles'; + +const styles = { + price: { color: 'purple' }, +}; + +const PriceField = withStyles(styles)(({ classes, ...props }) => ( + <TextField className={classes.price} {...props} /> +)); + +export const ProductList = (props) => ( + <List {...props}> + <Datagrid> + <PriceField source="price" /> + </Datagrid> + </List> +); + +// renders in the datagrid as +<td><span class="[class name generated by JSS]">2</span></td> +``` + +React-admin usually delegates the rendering of fields components to material-ui components. Refer to the material-ui documentation to see the default styles for elements. + +You may want to customize the cell style inside a `DataGrid`. You can use the `cellClassName` for that: + +```jsx +import { withStyles } from '@material-ui/core/styles'; + +const styles = { + priceCell: { fontWeight: 'bold' }, +}; + +const PriceField = withStyles(styles)(({ classes, ...props }) => ( + <TextField cellClassName={classes.priceCell} {...props} /> +)); + +export const ProductList = (props) => ( + <List {...props}> + <Datagrid> + <PriceField source="price" /> + </Datagrid> + </List> +); + +// renders in the datagrid as +<td class="[class name generated by JSS]"><span>2</span></td> +``` + +You may want to override the field header (the `<th>` element in the datagrid). In that case, use the `headerClassName` prop: + +```jsx +import { withStyles } from '@material-ui/core/styles'; + +const styles = { + priceHeader: { fontWeight: 'bold' }, +}; + +const PriceField = withStyles(styles)(({ classes, ...props }) => ( + <TextField headerClassName={classes.priceHeader} {...props} /> +)); + +export const ProductList = (props) => ( + <List {...props}> + <Datagrid> + <PriceField source="price" /> + </Datagrid> + </List> +); +// renders in the table header as +<th class="[class name generated by JSS]"><button>Price</button></td> +``` + +Finally, sometimes, you just want to right align the text of a cell. Use the `textAlign` prop, which accepts either `left` or `right`: + +```jsx +const PriceField = props => ( + <TextField {...props} /> +); + +PriceField.defaultProps = { + textAlign: 'right', +}; +``` + +## Writing Your Own Field Component + +If you don't find what you need in the list above, you can write your own Field component. It must be a regular React component, accepting not only a `source` attribute, but also a `record` attribute. React-admin will inject the `record` based on the API response data at render time. The field component only needs to find the `source` in the `record` and display it. + +For instance, here is an equivalent of react-admin's `<TextField>` component: + +```jsx +import React from 'react'; +import PropTypes from 'prop-types'; + +const TextField = ({ source, record = {} }) => <span>{record[source]}</span>; + +TextField.propTypes = { + label: PropTypes.string, + record: PropTypes.object, + source: PropTypes.string.isRequired, +}; + +export default TextField; +``` + +**Tip**: The `label` attribute isn't used in the `render()` method, but react-admin uses it to display the table header. + +**Tip**: If you want to support deep field sources (e.g. source values like `author.name`), use [`lodash/get`](https://www.npmjs.com/package/lodash.get) to replace the simple object lookup: + +```jsx +import get from 'lodash/get'; +const TextField = ({ source, record = {} }) => <span>{get(record, source)}</span>; +``` + +If you are not looking for reusability, you can create even simpler components, with no attributes. Let's say an API returns user records with `firstName` and `lastName` properties, and that you want to display a full name in a user list. + +```jsx +{ + id: 123, + firstName: 'John', + lastName: 'Doe' +} +``` + +The component will be: + +```jsx +import React from 'react'; +import { List, Datagrid, TextField } from 'react-admin'; + +const FullNameField = ({ record = {} }) => <span>{record.firstName} {record.lastName}</span>; +FullNameField.defaultProps = { label: 'Name' }; + +export const UserList = (props) => ( + <List {...props}> + <Datagrid> + <FullNameField source="lastName" /> + </Datagrid> + </List> +); +``` + +**Tip**: In such custom fields, the `source` is optional. React-admin uses it to determine which column to use for sorting when the column header is clicked. In case you use the `source` property for additional purposes, the sorting can be overridden by the `sortBy` property on any `Field` component. + +## Adding Label To Custom Field Components In The Show View + +React-admin lets you use the same Field components in the List view and in the Show view. But if you use the `<FullNameField>` custom field component defined earlier in a Show view, something is missing: the Field label. Why do other fields have a label and not this custom Field? And how can you create a Field component that has a label in the Show view, but not in the List view? + +React-admin uses a trick: the Show view layouts (`<SimpleShowLayout>` and `<TabbedShowLayout>`) inspect their Field children, and whenever one has the `addLabel` prop set to `true`, the layout adds a label. + +That means that the only thing you need to add to a custom component to make it usable in a Show view is a `addLabel: true` default prop. + +```jsx +FullNameField.defaultProps = { + addLabel: true, +}; +``` + +## Hiding A Field Based On The Value Of Another + +In a Show view, you may want to display or hide fields based on the value of another field - for instance, show an `email` field only if the `hasEmail` boolean field is `true`. + +For such cases, you can use the custom field approach: use the injected `record` prop, and render another Field based on the value. + +```jsx +import React from 'react'; +import { EmailField } from 'react-admin'; + +const ConditionalEmailField = ({ record, ...rest }) => + record && record.hasEmail + ? <EmailField source="email" record={record} {...rest} /> + : null; + +export default ConditionalEmailField; +``` + +**Tip**: Always check that the `record` is defined before inspecting its properties, as react-admin displays the Show view *before* fetching the record from the data provider. So the first time it renders the show view for a resource, the `record` is undefined. + +This `ConditionalEmailField` is properly hidden when `hasEmail` is false. But when `hasEmail` is true, the Show layout renders it... without label. And if you add a `addLabel` default prop, the Show layout will render the label regardless of the `hasEmail` value... + +One solution is to add the label manually in the custom component: + +```jsx +import React from 'react'; +import { Labeled, EmailField } from 'react-admin'; + +const ConditionalEmailField = ({ record, ...rest }) => + record && record.hasEmail + ? ( + <Labeled label="Email"> + <EmailField source="email" record={record} {...rest} /> + </Labeled> + ) + : null; + +export default ConditionalEmailField; +``` + +This comes with a drawback, though: the `<ConditionalEmailField>` cannot be used in a List view anymore, as it will always have a label. If you want to reuse the custom component in a List, this isn't the right solution. + +An alternative solution is to split the `<Show>` component. Under the hood, the `<Show>` component is composed of two sub components: the `<ShowController>` component, which fetches the record, and the `<ShowView>`, which is responsible for rendering the view title, actions, and children. `<ShowController>` uses the *render props* pattern: + +```jsx +// inside react-admin +const Show = props => ( + <ShowController {...props}> + {controllerProps => <ShowView {...props} {...controllerProps} />} + </ShowController> +); +``` + +The `<ShowController>` fetches the `record` from the data provider, and passes it to its child function when received (among the `controllerProps`). That means the following code: + +```jsx +import { Show, SimpleShowLayout, TextField } from 'react-admin'; + +const UserShow = props => ( + <Show {...props}> + <SimpleShowLayout> + <TextField source="username" /> + <TextField source="email" /> + </SimpleShowLayout> + </Show> +); +``` + +Is equivalent to: + +```jsx +import { ShowController, ShowView, SimpleShowLayout, TextField } from 'react-admin'; + +const UserShow = props => ( + <ShowController {...props}> + {controllerProps => + <ShowView {...props} {...controllerProps}> + <SimpleShowLayout> + <TextField source="username" /> + <TextField source="email" /> + </SimpleShowLayout> + </ShowView> + } + </ShowController> +); +``` + +If you want one field to be displayed based on the `record`, for instance to display the email field only if the `hasEmail` field is `true`, you just need to test the value from `controllerProps.record`, as follows: + +```jsx +import { ShowController, ShowView, SimpleShowLayout, TextField } from 'react-admin'; + +const UserShow = props => ( + <ShowController {...props}> + {controllerProps => + <ShowView {...props} {...controllerProps}> + <SimpleShowLayout> + <TextField source="username" /> + {controllerProps.record && controllerProps.record.hasEmail && + <TextField source="email" /> + } + </SimpleShowLayout> + </ShowView> + } + </ShowController> +); +``` + +And now you can use a regular Field component, and the label displays correctly in the Show view. \ No newline at end of file From c636a68a3464089d7add570b7ea5a6d9cf6fff36 Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:14 +0800 Subject: [PATCH 13/20] New translations faq.md (Chinese Simplified) --- website/translated_docs/zh-CN/faq.md | 96 ++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 website/translated_docs/zh-CN/faq.md diff --git a/website/translated_docs/zh-CN/faq.md b/website/translated_docs/zh-CN/faq.md new file mode 100644 index 0000000..aef36bf --- /dev/null +++ b/website/translated_docs/zh-CN/faq.md @@ -0,0 +1,96 @@ +--- +id: faq +title: FAQ +--- +- [Can I have custom identifiers/primary keys for my resources?](#can-i-have-custom-identifiersprimary-keys-for-my-resources) +- [I get warning about unique key for child in array](#i-get-warning-about-unique-key-for-child-in-array) +- [A form with validation freezes when rendering](#a-form-with-validation-freezes-when-rendering) +- [How can I customize the UI depending on the user permissions?](#how-can-i-customize-the-ui-depending-on-the-user-permissions) +- [How can I customize forms depending on its inputs values?](#how-can-i-customize-forms-depending-on-its-inputs-values) + +## Can I have custom identifiers/primary keys for my resources? + +React-admin requires that each resource has an `id` field to identify it. If your API uses a different name for the primary key, you have to map that name to `id` in a custom [dataProvider](./DataProviders.md). For instance, to use a field named `_id` as identifier: + +```js +const convertHTTPResponse = (response, type, resource, params) => { + const { headers, json } = response; + switch (type) { + case GET_LIST: + return { + data: json.map(resource => ({ ...resource, id: resource._id }) ), + total: parseInt(headers.get('content-range').split('/').pop(), 10), + }; + case UPDATE: + case DELETE: + case GET_ONE: + return { ...json, id: json._id }; + case CREATE: + return { ...params.data, id: json._id }; + default: + return json; + } +}; +``` + +## I get warning about unique key for child in array + +When displaying a `Datagrid` component, you get the following warning: + +> Warning: Each child in an array or iterator should have a unique "key" prop. Check the render method of `DatagridBody`. + +This is most probably because the resource does not have an `id` property as expected by react-admin. See the previous FAQ to see how to resolve this: [Can I have custom identifiers/primary keys for my resources?](#can-i-have-custom-identifiersprimary-keys-for-my-resources) + +## A form with validation freezes when rendering + +You're probably using validator factories directly in the render method of your component: + +```jsx +export const CommentEdit = ({ ...props }) => ( + <Edit {...props}> + <SimpleForm> + <DisabledInput source="id" /> + <DateInput source="created_at" /> + <LongTextInput source="body" validate={minLength(10)} /> + </SimpleForm> + </Edit> +); +``` + +Avoid calling functions directly inside the render method: + +```jsx +const validateMinLength = minLength(10); + +export const CommentEdit = ({ ...props }) => ( + <Edit {...props}> + <SimpleForm> + <DisabledInput source="id" /> + <DateInput source="created_at" /> + <LongTextInput source="body" validate={validateMinLength} /> + </SimpleForm> + </Edit> +); +``` + +This is related to [redux-form](https://github.com/erikras/redux-form/issues/3288). + +## How can I customize the UI depending on the user permissions? + +Some fairly common use cases which may be dependent on the user permissions: + +- Specific views +- Having parts of a view (fields, inputs) differents for specific users +- Hiding or displaying menu items + +For all those cases, you can use the [aor-permissions](https://github.com/marmelab/aor-permissions) addon. + +## How can I customize forms depending on its inputs values? + +Some use cases: + +- Show/hide some inputs if another input has a value +- Show/hide some inputs if another input has a specific value +- Show/hide some inputs if the current form values matches specific constraints + +For all those cases, you can use the [aor-dependent-input](https://github.com/marmelab/aor-dependent-input) addon. \ No newline at end of file From 9f6d4ae909650e36e42ee3761c9492c2a0526008 Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:15 +0800 Subject: [PATCH 14/20] New translations ecosystem.md (Chinese Simplified) --- website/translated_docs/zh-CN/ecosystem.md | 40 ++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 website/translated_docs/zh-CN/ecosystem.md diff --git a/website/translated_docs/zh-CN/ecosystem.md b/website/translated_docs/zh-CN/ecosystem.md new file mode 100644 index 0000000..78a0b47 --- /dev/null +++ b/website/translated_docs/zh-CN/ecosystem.md @@ -0,0 +1,40 @@ +--- +id: ecosystem +title: Ecosystem +--- +- [Inputs and Fields](#inputs-and-fields) +- [Translations](#translations) +- [Data Providers](#data-providers) +- [Miscellaneous](#miscellaneous) + +## Inputs and Fields + +- [vascofg/react-admin-color-input](https://github.com/vascofg/react-admin-color-input): a color input using [React Color](http://casesandberg.github.io/react-color/), a collection of color pickers. +- [LoicMahieu/aor-tinymce-input](https://github.com/LoicMahieu/aor-tinymce-input): a TinyMCE component, useful for editing HTML +- [vascofg/react-admin-date-inputs](https://github.com/vascofg/react-admin-date-inputs): a collection of Date Inputs, based on [material-ui-pickers](https://material-ui-pickers.firebaseapp.com/) + +## Translations + +See the [translation](./Translation.md#available-locales) page. + +## Data Providers + +- **[Epilogue](https://github.com/dchester/epilogue)**: [dunghuynh/aor-epilogue-client](https://github.com/dunghuynh/aor-epilogue-client) +- **[Feathersjs](http://www.feathersjs.com/)**: [josx/aor-feathers-client](https://github.com/josx/aor-feathers-client) +- **[Firebase](https://firebase.google.com/)**: [sidferreira/aor-firebase-client](https://github.com/sidferreira/aor-firebase-client) +- **[GraphQL](http://graphql.org/)**: [marmelab/ra-data-graphql](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-graphql) (uses [Apollo](http://www.apollodata.com/)) +- **[GraphCool](http://www.graph.cool/)**: [marmelab/ra-data-graphcool](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-graphql) (uses [Apollo](http://www.apollodata.com/)) +- **[Hydra](http://www.hydra-cg.com/) / [JSON-LD](https://json-ld.org/)**: [api-platform/admin/hydra](https://github.com/api-platform/admin/blob/master/src/hydra/hydraClient.js) +- **[JSON API](http://jsonapi.org/)**: [moonlight-labs/aor-jsonapi-client](https://github.com/moonlight-labs/aor-jsonapi-client) +- **[JSON server](https://github.com/typicode/json-server)**: [marmelab/ra-data-json-server](https://github.com/marmelab/ra-data-json-server). +- Local JSON: [marmelab/ra-data-fakerest](https://github.com/marmelab/ra-data-fakerest) +- **[Loopback](http://loopback.io/)**: [kimkha/aor-loopback](https://github.com/kimkha/aor-loopback) +- **[Parse Server](https://github.com/ParsePlatform/parse-server)**: [leperone/aor-parseserver-client](https://github.com/leperone/aor-parseserver-client) +- **[PostgREST](http://postgrest.com/en/v0.4/)**: [tomberek/aor-postgrest-client](https://github.com/tomberek/aor-postgrest-client) +- Simple REST: [marmelab/ra-data-simple-rest](https://github.com/marmelab/ra-data-simple-rest). + +## Miscellaneous + +- [api-platform/admin](https://api-platform.com/docs/admin): create a fully featured admin using React Admin for API supporting the [Hydra Core Vocabulary](http://www.hydra-cg.com/), including but not limited to APIs created using the [API Platform framework](https://api-platform.com) +- [marmelab/ra-realtime](https://github.com/marmelab/react-admin/tree/master/packages/ra-realtime): enable realtime updates +- [zifnab87/ra-component-factory](https://github.com/zifnab87/ra-component-factory): centralized configuration of immutability/visibility of fields/menu-links/action buttons, easy re-ordering of fields/properties and tab reorganization based on permission roles \ No newline at end of file From a2a08730a782a95cd7d8852221cd3f287e44d42c Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:16 +0800 Subject: [PATCH 15/20] New translations data-providers.md (Chinese Simplified) --- .../translated_docs/zh-CN/data-providers.md | 660 ++++++++++++++++++ 1 file changed, 660 insertions(+) create mode 100644 website/translated_docs/zh-CN/data-providers.md diff --git a/website/translated_docs/zh-CN/data-providers.md b/website/translated_docs/zh-CN/data-providers.md new file mode 100644 index 0000000..d2854f3 --- /dev/null +++ b/website/translated_docs/zh-CN/data-providers.md @@ -0,0 +1,660 @@ +--- +id: data-providers +title: Data Providers +--- +React-admin can communicate with any API, whether it uses REST, GraphQL, or even SOAP, regardless of the dialect it uses. For REST servers, it can be [JSON API](http://jsonapi.org/), [HAL](http://stateless.co/hal_specification.html), [OData](http://www.odata.org/) or a custom dialect. The only thing react-admin needs is a Data Provider function. This is the place to translate data queries to HTTP requests, and HTTP responses to data responses. + +![Data Provider architecture](https://marmelab.com/react-admin/img/data-provider.png) + +The `dataProvider` parameter of the `<Admin>` component must be a function with the following signature: + +```jsx +/** + * Query a data provider and return a promise for a response + * + * @example + * dataProvider(GET_ONE, 'posts', { id: 123 }) + * => Promise.resolve({ data: { id: 123, title: "hello, world" } }) + * + * @param {string} type Request type, e.g GET_LIST + * @param {string} resource Resource name, e.g. "posts" + * @param {Object} payload Request parameters. Depends on the action type + * @returns {Promise} the Promise for a response + */ +const dataProvider = (type, resource, params) => new Promise(); +``` + +You can find a Data Provider example implementation in [`packages/ra-data-simple-rest/src/index.js`](https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/src/index.js); + +The `dataProvider` is also the ideal place to add custom HTTP headers, authentication, etc. + +## Available Providers + +The react-admin project includes 4 Data Providers: + +* Simple REST: [marmelab/ra-data-simple-rest](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-simple-rest) ([read more below](#simple-rest)). It serves mostly as an example. Incidentally, it is compatible with the [FakeRest](https://github.com/marmelab/FakeRest) API. +* **[JSON server](https://github.com/typicode/json-server)**: [marmelab/ra-data-json-server](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-json-server). Great for prototyping an admin over a yet-to-be-developed REST API. +* [Graphcool](https://www.graph.cool/): [marmelab/ra-data-graphcool](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-graphcool). A provider for GraphQL servers following the Graphcool convention. Incidentally, this package builds up on [marmelab/ra-data-graphql](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-graphql), which lets you develop providers for other GraphQL conventions. +* Local JSON: [marmelab/ra-data-fakerest](https://github.com/marmelab/react-admin/tree/master/packages/ra-data-fakerest). Based on a local object, it doesn't even use HTTP. Use it for testing purposes. + +You can find Data Providers for various backends in third-party repositories: + +* **[DynamoDb](https://github.com/abiglobalhealth/aor-dynamodb-client)**: [abiglobalhealth/aor-dynamodb-client](https://github.com/abiglobalhealth/aor-dynamodb-client) +* **[Epilogue](https://github.com/dchester/epilogue)**: [dunghuynh/aor-epilogue-client](https://github.com/dunghuynh/aor-epilogue-client) +* **[Feathersjs](http://www.feathersjs.com/)**: [josx/aor-feathers-client](https://github.com/josx/aor-feathers-client) +* **[Firebase](https://firebase.google.com/)**: [sidferreira/aor-firebase-client](https://github.com/sidferreira/aor-firebase-client) +* **[JSON API](http://jsonapi.org/)**: [moonlight-labs/aor-jsonapi-client](https://github.com/moonlight-labs/aor-jsonapi-client) +* **[Loopback](http://loopback.io/)**: [kimkha/aor-loopback](https://github.com/kimkha/aor-loopback) +* **[Parse Server](https://github.com/ParsePlatform/parse-server)**: [leperone/aor-parseserver-client](https://github.com/leperone/aor-parseserver-client) +* **[PostgREST](http://postgrest.com/en/v0.4/)**: [tomberek/aor-postgrest-client](https://github.com/tomberek/aor-postgrest-client) +* **[Xmysql](https://github.com/o1lab/xmysql)**: [soaserele/aor-xmysql](https://github.com/soaserele/aor-xmysql) + +If you've written a Data Provider for another backend, and open-sourced it, please help complete this list with your package. + +## Usage + +As an example, let's focus on the Simple REST data provider. It fits REST APIs using simple GET parameters for filters and sorting. + +Install the `ra-data-simple-rest` package to use this provider. + +```sh +npm install ra-data-simple-rest +``` + +Then, initialize the provider with the RESt backend URL, and pass the result to the `dataProvider` prop of the `<Admin>` component: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; + +import { PostList } from './posts'; + +const App = () => ( + <Admin dataProvider={simpleRestProvider('http://path.to.my.api/')}> + <Resource name="posts" list={PostList} /> + </Admin> +); + +export default App; +``` + +Here is how this provider maps request types to API calls: + +| Request type | API calls | +| -------------------- | --------------------------------------------------------------------------------------------- | +| `GET_LIST` | `GET http://my.api.url/posts?sort=['title','ASC']&range=[0, 24]&filter={title:'bar'}` | +| `GET_ONE` | `GET http://my.api.url/posts/123` | +| `CREATE` | `POST http://my.api.url/posts/123` | +| `UPDATE` | `PUT http://my.api.url/posts/123` | +| `UPDATE_MANY` | Multiple calls to `PUT http://my.api.url/posts/123` | +| `DELETE` | `DELETE http://my.api.url/posts/123` | +| `DELETE_MANY` | Multiple calls to `DELETE http://my.api.url/posts/123` | +| `GET_MANY` | `GET http://my.api.url/posts?filter={ids:[123,456,789]}` | +| `GET_MANY_REFERENCE` | `GET http://my.api.url/posts?filter={author_id:345}` | + +**Note**: The simple REST client expects the API to include a `Content-Range` header in the response to `GET_LIST` calls. The value must be the total number of resources in the collection. This allows react-admin to know how many pages of resources there are in total, and build the pagination controls. + + Content-Range: posts 0-24/319 + + +If your API is on another domain as the JS code, you'll need to whitelist this header with an `Access-Control-Expose-Headers` [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS) header. + + Access-Control-Expose-Headers: Content-Range + + +## Adding Custom Headers + +The `simpleRestProvider` function accepts an HTTP client function as second argument. By default, it uses react-admin's `fetchUtils.fetchJson()` as HTTP client. It's similar to HTML5 `fetch()`, except it handles JSON decoding and HTTP error codes automatically. + +That means that if you need to add custom headers to your requests, you can just *wrap* the `fetchJson()` call inside your own function: + +```jsx +import { fetchUtils, Admin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; + +const httpClient = (url, options = {}) => { + if (!options.headers) { + options.headers = new Headers({ Accept: 'application/json' }); + } + // add your own headers here + options.headers.set('X-Custom-Header', 'foobar'); + return fetchUtils.fetchJson(url, options); +} +const dataProvider = simpleRestProvider('http://path.to.my.api/', httpClient); + +const App = () => ( + <Admin dataProvider={dataProvider}> + <Resource name="posts" list={PostList} /> + </Admin> +); +``` + +Now all the requests to the REST API will contain the `X-Custom-Header: foobar` header. + +**Tip**: The most common usage of custom headers is for authentication. `fetchJson` has built-on support for the `Authorization` token header: + +```jsx +const httpClient = (url, options = {}) => { + options.user = { + authenticated: true, + token: 'SRTRDFVESGNJYTUKTYTHRG' + } + return fetchUtils.fetchJson(url, options); +} +const dataProvider = simpleRestProvider('http://path.to.my.api/', httpClient); +``` + +Now all the requests to the REST API will contain the `Authorization: SRTRDFVESGNJYTUKTYTHRG` header. + +## Decorating your Data Provider (Example of File Upload) + +Instead of writing your own Data Provider, you can enhance the capabilities of an existing data provider. You can even restrict the customization on a given resource. + +For instance, if you want to use upload components (such as `<ImageInput />` one), you can decorate the provider the following way: + +```jsx +// in addUploadFeature.js +/** + * Convert a `File` object returned by the upload input into a base 64 string. + * That's not the most optimized way to store images in production, but it's + * enough to illustrate the idea of data provider decoration. + */ +const convertFileToBase64 = file => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(file.rawFile); + + reader.onload = () => resolve(reader.result); + reader.onerror = reject; +}); + +/** + * For posts update only, convert uploaded image in base 64 and attach it to + * the `picture` sent property, with `src` and `title` attributes. + */ +const addUploadFeature = requestHandler => (type, resource, params) => { + if (type === 'UPDATE' && resource === 'posts') { + if (params.data.pictures && params.data.pictures.length) { + // only freshly dropped pictures are instance of File + const formerPictures = params.data.pictures.filter(p.rawFile => !(p instanceof File)); + const newPictures = params.data.pictures.filter(p.rawFile => p instanceof File); + + return Promise.all(newPictures.map(convertFileToBase64)) + .then(base64Pictures => base64Pictures.map(picture64 => ({ + src: picture64, + title: `${params.data.title}`, + }))) + .then(transformedNewPictures => requestHandler(type, resource, { + ...params, + data: { + ...params.data, + pictures: [...transformedNewPictures, ...formerPictures], + }, + })); + } + } + // for other request types and reources, fall back to the defautl request handler + return requestHandler(type, resource, params); +}; + +export default addUploadFeature; +``` + +To enhance a provider with the upload feature, compose `addUploadFeature` function with the data provider function: + +```jsx +import simpleRestProvider from 'ra-data-simple-rest'; +import addUploadFeature from './addUploadFeature'; + +const dataProvider = simpleRestProvider('http://path.to.my.api/'); +const uploadCapableDataProvider = addUploadFeature(dataProvider); + +const App = () => ( + <Admin dataProvider={uploadCapableDataProvider}> + <Resource name="posts" list={PostList} /> + </Admin> +); +``` + +## Writing Your Own Data Provider + +Quite often, there is no Data Provider that suits you API - either in the core providers, or in the third-party providers. In such cases, you'll have to write your own Data Provider. + +A Data Provider is a function that receives a request, and returns a promise for a response. Both the request and the response format are standardized. + +```jsx +/** + * Query a data provider and return a promise for a response + * + * @example + * dataProvider(GET_ONE, 'posts', { id: 123 }) + * => Promise.resolve({ data: { id: 123, title: "hello, world" } }) + * + * @param {string} type Request type, e.g GET_LIST + * @param {string} resource Resource name, e.g. "posts" + * @param {Object} payload Request parameters. Depends on the action type + * @returns {Promise} the Promise for a response + */ +const dataProvider = (type, resource, params) => new Promise(); +``` + +When you write a Data provider, your job is to route requests to your API backend(s), then transform their response to match the format returned by the Data Provider. + +### Request Format + +Data queries require a *type* (e.g. `GET_ONE`), a *resource* (e.g. 'posts') and a set of *parameters*. + +*Tip*: In comparison, HTTP requests require a *verb* (e.g. 'GET'), an *url* (e.g. 'http://myapi.com/posts'), a list of *headers* (like `Content-Type`) and a *body*. + +Possible types are: + +| Type | Usage | Params format | +| -------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET_LIST` | Search for resources | `{ pagination: { page: {int} , perPage: {int} }, sort: { field: {string}, order: {string} }, filter: {Object} }` | +| `GET_ONE` | Read a single resource, by id | `{ id: {mixed} }` | +| `CREATE` | Create a single resource | `{ data: {Object} }` | +| `UPDATE` | Update a single resource | `{ id: {mixed}, data: {Object}, previousData: {Object} }` | +| `UPDATE_MANY` | Update multiple resources | `{ ids: {mixed[]}, data: {Object} }` | +| `DELETE` | Delete a single resource | `{ id: {mixed}, previousData: {Object} }` | +| `DELETE_MANY` | Delete multiple resources | `{ ids: {mixed[]} }` | +| `GET_MANY` | Read a list of resource, by ids | `{ ids: {mixed[]} }` | +| `GET_MANY_REFERENCE` | Read a list of resources related to another one | `{ target: {string}, id: {mixed}, pagination: { page: {int} , perPage: {int} }, sort: { field: {string}, order: {string} }, filter: {Object} }` | + +Here are several examples of how react-admin can call the Data Provider with these types: + +```jsx +dataProvider(GET_LIST, 'posts', { + pagination: { page: 1, perPage: 5 }, + sort: { field: 'title', order: 'ASC' }, + filter: { author_id: 12 }, +}); +dataProvider(GET_ONE, 'posts', { id: 123 }); +dataProvider(CREATE, 'posts', { data: { title: "hello, world" } }); +dataProvider(UPDATE, 'posts', { + id: 123, + data: { title: "hello, world!" }, + previousData: { title: "previous title" } +}); +dataProvider(UPDATE_MANY, 'posts', { + ids: [123, 234], + data: { views: 0 }, +}); +dataProvider(DELETE, 'posts', { + id: 123, + previousData: { title: "hello, world" } +}); +dataProvider(DELETE_MANY, 'posts', { ids: [123, 234] }); +dataProvider(GET_MANY, 'posts', { ids: [123, 124, 125] }); +dataProvider(GET_MANY_REFERENCE, 'comments', { + target: 'post_id', + id: 123, + sort: { field: 'created_at', order: 'DESC' } +}); +``` + +### Example Request Processing + +Let's say that you want to map the Data Provider requests to a REST backend, like so: + +* `GET_LIST => GET http://path.to.my.api/posts?sort=["title","ASC"]&range=[0, 24]&filter={"author_id":12}` +* `GET_ONE => GET http://path.to.my.api/posts/123` +* `CREATE => POST http://path.to.my.api/posts` +* `UPDATE => PUT http://path.to.my.api/posts/123` +* `UPDATE_MANY => PUT http://path.to.my.api/posts?filter={"ids":[123,124,125]}` +* `DELETE => DELETE http://path.to.my.api/posts/123` +* `DELETE_MANY => DELETE http://path.to.my.api/posts?filter={"ids":[123,124,125]}` +* `GET_MANY => GET http://path.to.my.api/posts?filter={"ids":[123,124,125]}` +* `GET_MANY_REFERENCE => GET http://path.to.my.api/comments?sort=["created_at","DESC"]&range=[0, 24]&filter={"post_id":123}` + +Data Providers often use a `switch` statement, and finish by a call to `fetch()`. Here is an example implementation: + +```js +// in myRestProvider.js +import { stringify } from 'query-string'; +import { + GET_LIST, + GET_ONE, + CREATE, + UPDATE, + DELETE, + GET_MANY, + GET_MANY_REFERENCE, +} from 'react-admin'; + +const apiUrl = 'http://path.to.my.api/'; + +/** + * Maps react-admin queries to my REST API + * + * @param {string} type Request type, e.g GET_LIST + * @param {string} resource Resource name, e.g. "posts" + * @param {Object} payload Request parameters. Depends on the request type + * @returns {Promise} the Promise for a data response + */ +export default (type, resource, params) => { + let url = ''; + const options = { + headers : new Headers({ + Accept: 'application/json', + }), + }; + switch (type) { + case GET_LIST: { + const { page, perPage } = params.pagination; + const { field, order } = params.sort; + const query = { + sort: JSON.stringify([field, order]), + range: JSON.stringify([ + (page - 1) * perPage, + page * perPage - 1, + ]), + filter: JSON.stringify(params.filter), + }; + url = `${apiUrl}/${resource}?${stringify(query)}`; + break; + } + case GET_ONE: + url = `${apiUrl}/${resource}/${params.id}`; + break; + case CREATE: + url = `${apiUrl}/${resource}`; + options.method = 'POST'; + options.body = JSON.stringify(params.data); + break; + case UPDATE: + url = `${apiUrl}/${resource}/${params.id}`; + options.method = 'PUT'; + options.body = JSON.stringify(params.data); + break; + case UPDATE_MANY: + const query = { + filter: JSON.stringify({ id: params.ids }), + }; + url = `${apiUrl}/${resource}?${stringify(query)}`; + options.method = 'PATCH'; + options.body = JSON.stringify(params.data); + break; + case DELETE: + url = `${apiUrl}/${resource}/${params.id}`; + options.method = 'DELETE'; + break; + case DELETE_MANY: + const query = { + filter: JSON.stringify({ id: params.ids }), + }; + url = `${apiUrl}/${resource}?${stringify(query)}`; + options.method = 'DELETE'; + break; + case GET_MANY: { + const query = { + filter: JSON.stringify({ id: params.ids }), + }; + url = `${apiUrl}/${resource}?${stringify(query)}`; + break; + } + case GET_MANY_REFERENCE: { + const { page, perPage } = params.pagination; + const { field, order } = params.sort; + const query = { + sort: JSON.stringify([field, order]), + range: JSON.stringify([ + (page - 1) * perPage, + page * perPage - 1, + ]), + filter: JSON.stringify({ + ...params.filter, + [params.target]: params.id, + }), + }; + url = `${apiUrl}/${resource}?${stringify(query)}`; + break; + } + default: + throw new Error(`Unsupported Data Provider request type ${type}`); + } + + return fetch(url, options) + .then(res => res.json()) + .then(response => + /* Convert HTTP Response to Data Provider Response */ + /* Covered in the next section */ + ); +}; +``` + +### Response Format + +React-admin expects responses from Data Providers to be objects with a `data` property. The data format depends on the request type. + +| Request Type | Response format | +| -------------------- | ----------------------------------------------------- | +| `GET_LIST` | `{ data: {Record[]}, total: {int} }` | +| `GET_ONE` | `{ data: {Record} }` | +| `CREATE` | `{ data: {Record} }` | +| `UPDATE` | `{ data: {Record} }` | +| `UPDATE_MANY` | `{ data: {mixed[]} }` The ids which have been updated | +| `DELETE` | `{ data: {Record} }` | +| `DELETE_MANY` | `{ data: {mixed[]} }` The ids which have been deleted | +| `GET_MANY` | `{ data: {Record[]} }` | +| `GET_MANY_REFERENCE` | `{ data: {Record[]}, total: {int} }` | + +A `{Record}` is an object literal with at least an `id` property, e.g. `{ id: 123, title: "hello, world" }`. + +Building up on the previous example, here are example responses matching the format expected by react-admin: + +```jsx +dataProvider(GET_LIST, 'posts', { + pagination: { page: 1, perPage: 5 }, + sort: { field: 'title', order: 'ASC' }, + filter: { author_id: 12 }, +}) +.then(response => console.log(response)); +// { +// data: [ +// { id: 126, title: "allo?", author_id: 12 }, +// { id: 127, title: "bien le bonjour", author_id: 12 }, +// { id: 124, title: "good day sunshine", author_id: 12 }, +// { id: 123, title: "hello, world", author_id: 12 }, +// { id: 125, title: "howdy partner", author_id: 12 }, +// ], +// total: 27 +// } + +dataProvider(GET_ONE, 'posts', { id: 123 }) +.then(response => console.log(response)); +// { +// data: { id: 123, title: "hello, world" } +// } + +dataProvider(CREATE, 'posts', { data: { title: "hello, world" } }) +.then(response => console.log(response)); +// { +// data: { id: 450, title: "hello, world" } +// } + +dataProvider(UPDATE, 'posts', { + id: 123, + data: { title: "hello, world!" }, + previousData: { title: "previous title" } +}) +.then(response => console.log(response)); +// { +// data: { id: 123, title: "hello, world!" } +// } + +dataProvider(UPDATE_MANY, 'posts', { + ids: [123, 234], + data: { views: 0 }, +}) +.then(response => console.log(response)); +// { +// data: [123, 234] +// } + +dataProvider(DELETE, 'posts', { + id: 123, + previousData: { title: "hello, world!" } +}) +.then(response => console.log(response)); +// { +// data: { id: 123, title: "hello, world" } +// } + +dataProvider(DELETE_MANY, 'posts', { ids: [123, 234] }) +.then(response => console.log(response)); +// { +// data: [123, 234] +// } + +dataProvider(GET_MANY, 'posts', { ids: [123, 124, 125] }) +.then(response => console.log(response)); +// { +// data: [ +// { id: 123, title: "hello, world" }, +// { id: 124, title: "good day sunshise" }, +// { id: 125, title: "howdy partner" }, +// ] +// } + +dataProvider(GET_MANY_REFERENCE, 'comments', { + target: 'post_id', + id: 123, + sort: { field: 'created_at', order: 'DESC' } +}); +.then(response => console.log(response)); +// { +// data: [ +// { id: 667, title: "I agree", post_id: 123 }, +// { id: 895, title: "I don't agree", post_id: 123 }, +// ], +// total: 2, +// } +``` + +### Example Response Processing + +Let's continue with the REST backend example. This backend returns responses as follows: + + GET http://path.to.my.api/posts?sort=['title','ASC']&range=[0, 4]&filter={author_id:12} + Content-Range: posts 0-4/27 + [ + { "id": 126, "title": "allo?", "author_id": 12 }, + { "id": 127, "title": "bien le bonjour", "author_id": 12 }, + { "id": 124, "title": "good day sunshine", "author_id": 12 }, + { "id": 123, "title": "hello, world", "author_id": 12 }, + { "id": 125, "title": "howdy partner", "author_id": 12 } + ] + + GET http://path.to.my.api/posts/123 + { "id": 123, "title": "hello, world", "author_id": 12 } + + POST http://path.to.my.api/posts + { "id": 123, "title": "hello, world", "author_id": 12 } + + PUT http://path.to.my.api/posts/123 + { "id": 123, "title": "hello, world", "author_id": 12 } + + PATCH http://path.to.my.api/posts?filter={ids:[123,124,125]} + [123, 124, 125] + + DELETE http://path.to.my.api/posts/123 + { "id": 123, "title": "hello, world", "author_id": 12 } + + DELETE http://path.to.my.api/posts?filter={ids:[123,124,125]} + [123, 124, 125] + + GET http://path.to.my.api/posts?filter={ids:[123,124,125]} + [ + { "id": 123, "title": "hello, world", "author_id": 12 }, + { "id": 124, "title": "good day sunshine", "author_id": 12 }, + { "id": 125, "title": "howdy partner", "author_id": 12 } + ] + + GET http://path.to.my.api/comments?sort=['created_at','DESC']&range=[0, 24]&filter={post_id:123} + Content-Range: comments 0-1/2 + [ + { "id": 667, "title": "I agree", "post_id": 123 }, + { "id": 895, "title": "I don't agree", "post_id": 123 } + ] + + +The Data Provider must therefore transform the response from the API backend to the expected response format. + +```js +// in myRestProvider.js +import { stringify } from 'query-string'; +import { + GET_LIST, + GET_ONE, + CREATE, + UPDATE, + UPDATE_MANY, + DELETE, + DELETE_MANY, + GET_MANY, + GET_MANY_REFERENCE, +} from 'react-admin'; + +const apiUrl = 'http://path.to.my.api/'; + +/** + * Maps react-admin queries to my REST API + * + * @param {string} type Request type, e.g GET_LIST + * @param {string} resource Resource name, e.g. "posts" + * @param {Object} payload Request parameters. Depends on the request type + * @returns {Promise} the Promise for a data response + */ +export default (type, resource, params) => { + let url = ''; + const options = { + headers : new Headers({ + Accept: 'application/json', + }), + }; + switch (type) { + /* Prepare url and options as above */ + } + + let headers; + return fetch(url, options) + .then(res => { + headers = res.headers; + return res.json(); + }) + .then(json => { + switch (type) { + case GET_LIST: + case GET_MANY_REFERENCE: + if (!headers.has('content-range')) { + throw new Error( + 'The Content-Range header is missing in the HTTP Response. The simple REST data provider expects responses for lists of resources to contain this header with the total number of results to build the pagination. If you are using CORS, did you declare Content-Range in the Access-Control-Expose-Headers header?' + ); + } + return { + data: json, + total: parseInt( + headers + .get('content-range') + .split('/') + .pop(), + 10 + ), + }; + case CREATE: + return { data: { ...params.data, id: json.id } }; + default: + return { data: json }; + } + }); +}; +``` + +### Error Format + +When the API backend returns an error, the Data Provider should `throw` an `Error` object. This object should contain a `status` property with the HTTP response code (404, 500, etc.). React-admin inspects this error code, and uses it for [authentication](./Authentication.md) (in case of 401 or 403 errors). Besides, react-admin displays the error `message` on screen in a temporary notification. + +### Example implementation + +Check the code from the [simple REST client](https://github.com/marmelab/react-admin/blob/master/packages/ra-data-simple-rest/src/index.js): it's a good starting point for a custom Data Provider implementation. \ No newline at end of file From a093e17acb57aac227ad344e253ffc52c489bcd2 Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:17 +0800 Subject: [PATCH 16/20] New translations custom-app.md (Chinese Simplified) --- website/translated_docs/zh-CN/custom-app.md | 109 ++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 website/translated_docs/zh-CN/custom-app.md diff --git a/website/translated_docs/zh-CN/custom-app.md b/website/translated_docs/zh-CN/custom-app.md new file mode 100644 index 0000000..f4268bd --- /dev/null +++ b/website/translated_docs/zh-CN/custom-app.md @@ -0,0 +1,109 @@ +--- +id: custom-app +title: Including the Admin in Another App +--- +The `<Admin>` tag is a great shortcut got be up and running with react-admin in minutes. However, in many cases, you will want to embed the admin in another application, or customize the admin deeply. Fortunately, you can do all the work that `<Admin>` does on any React application. + +Beware that you need to know about [redux](http://redux.js.org/), [react-router](https://github.com/reactjs/react-router), and [redux-saga](https://github.com/yelouafi/redux-saga) to go further. + +**Tip**: Before going for the Custom App route, explore all the options of [the `<Admin>` component](./Admin.md). They allow you to add custom routes, custom reducers, custom sagas, and customize the layout. + +Here is the main code for bootstrapping a barebones react-admin application with 3 resources: `posts`, `comments`, and `users`: + +```jsx +// in src/App.js +import React from 'react'; +import PropTypes from 'prop-types'; +import { render } from 'react-dom'; + +// redux, react-router, redux-form, saga, and material-ui +// form the 'kernel' on which react-admin runs +import { combineReducers, createStore, compose, applyMiddleware } from 'redux'; +import { Provider } from 'react-redux'; +import createHistory from 'history/createHashHistory'; +import { Switch, Route } from 'react-router-dom'; +import { ConnectedRouter, routerReducer, routerMiddleware } from 'react-router-redux'; +import { reducer as formReducer } from 'redux-form'; +import createSagaMiddleware from 'redux-saga'; +import { MuiThemeProvider } from '@material-ui/core/styles'; +import AppBar from '@material-ui/core/AppBar'; +import Toolbar from '@material-ui/core/Toolbar'; +import Typography from '@material-ui/core/Typography'; + +// prebuilt react-admin features +import { + adminReducer, + i18nReducer, + adminSaga, + TranslationProvider, +} from 'react-admin'; +import restProvider from 'ra-data-simple-rest'; +import defaultMessages from 'ra-language-english'; + +// your app components +import Dashboard from './Dashboard'; +import { PostList, PostCreate, PostEdit, PostShow } from './Post'; +import { CommentList, CommentEdit, CommentCreate } from './Comment'; +import { UserList, UserEdit, UserCreate } from './User'; +// your app labels +import messages from './i18n'; + +// create a Redux app +const reducer = combineReducers({ + admin: adminReducer, + i18n: i18nReducer('en', messages['en']), + form: formReducer, + routing: routerReducer, +}); +const sagaMiddleware = createSagaMiddleware(); +const history = createHistory(); +const store = createStore(reducer, undefined, compose( + applyMiddleware(sagaMiddleware, routerMiddleware(history)), + window.devToolsExtension ? window.devToolsExtension() : f => f, +)); + +// side effects +const dataProvider = restProvider('http://path.to.my.api/'); +const authProvider = () => Promise.resolve(); +const i18nProvider = locale => { + if (locale !== 'en') { + return messages[locale]; + } + return defaultMessages; +}; +sagaMiddleware.run(adminSaga(dataProvider, authProvider, i18nProvider)); + +// bootstrap redux and the routes +const App = () => ( + <Provider store={store}> + <TranslationProvider> + <ConnectedRouter history={history}> + <MuiThemeProvider> + <AppBar position="static" color="default"> + <Toolbar> + <Typography variant="title" color="inherit"> + My admin + </Typography> + </Toolbar> + </AppBar> + <Switch> + <Route exact path="/" component={Dashboard} /> + <Route exact path="/posts" hasCreate render={(routeProps) => <PostList resource="posts" {...routeProps} />} /> + <Route exact path="/posts/create" render={(routeProps) => <PostCreate resource="posts" {...routeProps} />} /> + <Route exact path="/posts/:id" hasShow render={(routeProps) => <PostEdit resource="posts" {...routeProps} />} /> + <Route exact path="/posts/:id/show" hasEdit render={(routeProps) => <PostShow resource="posts" {...routeProps} />} /> + <Route exact path="/comments" hasCreate render={(routeProps) => <CommentList resource="comments" {...routeProps} />} /> + <Route exact path="/comments/create" render={(routeProps) => <CommentCreate resource="comments" {...routeProps} />} /> + <Route exact path="/comments/:id" render={(routeProps) => <CommentEdit resource="comments" {...routeProps} />} /> + <Route exact path="/users" hasCreate render={(routeProps) => <UsersList resource="users" {...routeProps} />} /> + <Route exact path="/users/create" render={(routeProps) => <UsersCreate resource="users" {...routeProps} />} /> + <Route exact path="/users/:id" render={(routeProps) => <UsersEdit resource="users" {...routeProps} />} /> + </Switch> + </MuiThemeProvider> + </ConnectedRouter> + </TranslationProvider> + </Provider> +); +``` + +This application has no sidebar, no theming, no [auth control](./Authentication.md#restricting-access-to-a-custom-page) - it's up to you to add these. From there on, you can customize pretty much anything you want. \ No newline at end of file From a989f08906a4c35c934acc13efcc225d553a2192 Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:18 +0800 Subject: [PATCH 17/20] New translations creat-edit-view-components.md (Chinese Simplified) --- .../zh-CN/creat-edit-view-components.md | 687 ++++++++++++++++++ 1 file changed, 687 insertions(+) create mode 100644 website/translated_docs/zh-CN/creat-edit-view-components.md diff --git a/website/translated_docs/zh-CN/creat-edit-view-components.md b/website/translated_docs/zh-CN/creat-edit-view-components.md new file mode 100644 index 0000000..0499e2d --- /dev/null +++ b/website/translated_docs/zh-CN/creat-edit-view-components.md @@ -0,0 +1,687 @@ +--- +layout: creat-edit-view-components +title: <Create> and <Edit> Views +--- +The Create and Edit views both display a form, initialized with an empty record (for the Create view) or with a record fetched from the API (for the Edit view). The `<Create>` and `<Edit>` components then delegate the actual rendering of the form to a form component - usually `<SimpleForm>`. This form component uses its children ([`<Input>`](./Inputs.md) components) to render each form input. + +![post creation form](https://marmelab.com/react-admin/img/create-view.png) + +![post edition form](https://marmelab.com/react-admin/img/edit-view.png) + +## The `Create` and `Edit` components + +The `<Create>` and `<Edit>` components render the page title and actions, and fetch the record from the data provider. They are not responsible for rendering the actual form - that's the job of their child component (usually `<SimpleForm>`), to which they pass the `record` as prop. + +Here are all the props accepted by the `<Create>` and `<Edit>` components: + +* [`title`](#page-title) +* [`actions`](#actions) + +Here is the minimal code necessary to display a form to create and edit comments: + +```jsx +// in src/App.js +import React from 'react'; +import { Admin, Resource } from 'react-admin'; +import jsonServerProvider from 'ra-data-json-server'; + +import { PostCreate, PostEdit } from './posts'; + +const App = () => ( + <Admin dataProvider={jsonServerProvider('http://jsonplaceholder.typicode.com')}> + <Resource name="posts" create={PostCreate} edit={PostEdit} /> + </Admin> +); + +export default App; + +// in src/posts.js +import React from 'react'; +import { Create, Edit, SimpleForm, DisabledInput, TextInput, DateInput, LongTextInput, ReferenceManyField, Datagrid, TextField, DateField, EditButton } from 'react-admin'; +import RichTextInput from 'ra-input-rich-text'; + +export const PostCreate = (props) => ( + <Create {...props}> + <SimpleForm> + <TextInput source="title" /> + <TextInput source="teaser" options={{ multiLine: true }} /> + <RichTextInput source="body" /> + <DateInput label="Publication date" source="published_at" defaultValue={new Date()} /> + </SimpleForm> + </Create> +); + +export const PostEdit = (props) => ( + <Edit title={<PostTitle />} {...props}> + <SimpleForm> + <DisabledInput label="Id" source="id" /> + <TextInput source="title" validate={required()} /> + <LongTextInput source="teaser" validate={required()} /> + <RichTextInput source="body" validate={required()} /> + <DateInput label="Publication date" source="published_at" /> + <ReferenceManyField label="Comments" reference="comments" target="post_id"> + <Datagrid> + <TextField source="body" /> + <DateField source="created_at" /> + <EditButton /> + </Datagrid> + </ReferenceManyField> + </SimpleForm> + </Edit> +); +``` + +That's enough to display the post edit form: + +![post edition form](https://marmelab.com/react-admin/img/post-edition.png) + +**Tip**: You might find it cumbersome to repeat the same input components for both the `<Create>` and the `<Edit>` view. In practice, these two views almost never have exactly the same form inputs. For instance, in the previous snippet, the `<Edit>` views shows related comments to the current post, which makes no sense for a new post. Having two separate sets of input components for the two views is therefore a deliberate choice. However, if you have the same set of input components, export them as a custom Form component to avoid repetition. + +`<Create>` accepts a `record` prop, to initialize the form based on an value object. + +### Page Title + +By default, the title for the Create view is "Create [resource_name]", and the title for the Edit view is "Edit [resource_name] #[record_id]". + +You can customize this title by specifying a custom `title` prop: + +```jsx +export const PostEdit = (props) => ( + <Edit title="Post edition" {...props}> + ... + </Edit> +); +``` + +More interestingly, you can pass a component as `title`. React-admin clones this component and, in the `<EditView>`, injects the current `record`. This allows to customize the title according to the current record: + +```jsx +const PostTitle = ({ record }) => { + return <span>Post {record ? `"${record.title}"` : ''}</span>; +}; +export const PostEdit = (props) => ( + <Edit title={<PostTitle />} {...props}> + ... + </Edit> +); +``` + +### Actions + +You can replace the list of default actions by your own element using the `actions` prop: + +```jsx +import Button from '@material-ui/core/Button'; +import { + CardActions, + ListButton, + ShowButton, + DeleteButton, + RefreshButton, +} from 'react-admin'; + +const PostEditActions = ({ basePath, data, resource }) => ( + <CardActions> + <ShowButton basePath={basePath} record={data} /> + <ListButton basePath={basePath} /> + <DeleteButton basePath={basePath} record={data} resource={resource} /> + <RefreshButton /> + {/* Add your custom actions */} + <Button color="primary" onClick={customAction}>Custom Action</Button> + </CardActions> +); + +export const PostEdit = (props) => ( + <Edit actions={<PostEditActions />} {...props}> + ... + </Edit> +); +``` + +Using a custom `EditActions` component also allow to remove the `<DeleteButton>` if you want to prevent deletions from the admin. + +## Prefilling a `Create` Record + +By default, the `<Create>` view starts with an empty `record`. You can pass a custom `record` object to start with preset values: + +```jsx +const commentDefaultValue = { nb_views: 0 }; +export const CommentCreate = (props) => ( + <Create {...props} record={commentDefaultValue}> + <SimpleForm> + <TextInput source="author" /> + <RichTextInput source="body" /> + <NumberInput source="nb_views" /> + </SimpleForm> + </Create> +); +``` + +While using the `record` to set default values works here, it doesn't work with `<Edit>`. So it's recommended to use [the `defaultValue` prop in the Form component](#default-values) instead. + +However, there is a valid use case for presetting the `record` prop: to prepopulate a record based on a related record. For instance, in a `PostShow` component, you may want to display a button to create a comment related to the current post, that would lead to a `CommentCreate` page where the `post_id` is preset. + +To enable this, you must first update the `CommentCreate` component to read the record from the `location` object (which is injected by react-router): + +```diff +const commentDefaultValue = { nb_views: 0 }; +-export const CommentCreate = (props) => ( ++export const CommentCreate = ({ location, ...props}) => ( +- <Create {...props}> ++ <Create ++ record={(location.state && location.state.record) || defaultValue} ++ location={location} ++ {...props} ++ > + <SimpleForm> + <TextInput source="author" /> + <RichTextInput source="body" /> + <NumberInput source="nb_views" /> + </SimpleForm> + </Create> +); +``` + +To set this `location.state`, you have to create a link or a button using react-router's `<Link>` component: + +```jsx +// in PostShow.js +import Button from '@material-ui/core/Button'; +import { Link } from 'react-router-dom'; + +const CreateRelatedCommentButton = ({ record }) => ( + <Button + component={Link} + to={{ + pathname: '/comments/create', + state: { record: { post_id: record.id } }, + }} + > + Write a comment for that post + </Button> +); + +export default PostShow = props => ( + <Show {...props}> + <SimpleShowLayout> + ... + <CreateRelatedCommentButton /> + </SimpleShowLayout> + </Show> +) +``` + +**Tip**: To style the button with the main color from the material-ui theme, use the `Link` component from the `react-admin` package rather than the one from `react-router`. + +## The `SimpleForm` component + +The `<SimpleForm>` component receives the `record` as prop from its parent component. It is responsible for rendering the actual form. It is also responsible for validating the form data. Finally, it receives a `handleSubmit` function as prop, to be called with the updated record as argument when the user submits the form. + +The `<SimpleForm>` renders its child components line by line (within `<div>` components). It uses `redux-form`. + +![post edition form](https://marmelab.com/react-admin/img/post-edition.png) + +By default the `<SimpleForm>` submits the form when the user presses `ENTER`. If you want to change this behaviour you can pass `false` for the `submitOnEnter` property, and the user will only be able to submit by pressing the save button. This can be useful e.g. if you have an input widget using `ENTER` for a special function. + +Here are all the props accepted by the `<SimpleForm>` component: + +* [`defautValue`](#default-values) +* [`validate`](#validation) +* [`submitOnEnter`](#submit-on-enter) +* [`redirect`](#redirection-after-submission) +* [`toolbar`](#toolbar) +* `save`: The function invoked when the form is submitted. This is passed automatically by `react-admin` when the form component is used inside `Create` and `Edit` components. +* `saving`: A boolean indicating whether a save operation is ongoing. This is passed automatically by `react-admin` when the form component is used inside `Create` and `Edit` components. +* `form`: The name of the [`redux-form`](https://redux-form.com/7.4.2/docs/api/reduxform.md/#-code-form-string-code-required-). It defaults to `record-form` and should only be modified when using the `SimpleForm` outside of a `Create` or `Edit` component. + +```jsx +export const PostCreate = (props) => ( + <Create {...props}> + <SimpleForm> + <TextInput source="title" /> + <RichTextInput source="body" /> + <NumberInput source="nb_views" /> + </SimpleForm> + </Create> +); +``` + +## The `TabbedForm` component + +Just like `<SimpleForm>`, `<TabbedForm>` receives the `record` prop, renders the actual form, and handles form validation on submit. However, the `<TabbedForm>` component renders inputs grouped by tab. The tabs are set by using `<FormTab>` components, which expect a `label` and an `icon` prop. + +![tabbed form](https://marmelab.com/react-admin/img/tabbed-form.gif) + +By default the `<TabbedForm>` submits the form when the user presses `ENTER`, if you want to change this behaviour you can pass `false` for the `submitOnEnter` property. + +Here are all the props accepted by the `<TabbedForm>` component: + +* [`defautValue`](#default-values) +* [`validate`](#validation) +* [`submitOnEnter`](#submit-on-enter) +* [`redirect`](#redirection-after-submission) +* [`toolbar`](#toolbar) +* `save`: The function invoked when the form is submitted. This is passed automatically by `react-admin` when the form component is used inside `Create` and `Edit` components. +* `saving`: A boolean indicating whether a save operation is ongoing. This is passed automatically by `react-admin` when the form component is used inside `Create` and `Edit` components. +* `form`: The name of the [`redux-form`](https://redux-form.com/7.4.2/docs/api/reduxform.md/#-code-form-string-code-required-). It defaults to `record-form` and should only be modified when using the `TabbedForm` outside of a `Create` or `Edit` component. + +```jsx +import { TabbedForm, FormTab } from 'react-admin' + +export const PostEdit = (props) => ( + <Edit {...props}> + <TabbedForm> + <FormTab label="summary"> + <DisabledInput label="Id" source="id" /> + <TextInput source="title" validate={required()} /> + <LongTextInput source="teaser" validate={required()} /> + </FormTab> + <FormTab label="body"> + <RichTextInput source="body" validate={required()} addLabel={false} /> + </FormTab> + <FormTab label="Miscellaneous"> + <TextInput label="Password (if protected post)" source="password" type="password" /> + <DateInput label="Publication date" source="published_at" /> + <NumberInput source="average_note" validate={[ number(), minValue(0) ]} /> + <BooleanInput label="Allow comments?" source="commentable" defaultValue /> + <DisabledInput label="Nb views" source="views" /> + </FormTab> + <FormTab label="comments"> + <ReferenceManyField reference="comments" target="post_id" addLabel={false}> + <Datagrid> + <TextField source="body" /> + <DateField source="created_at" /> + <EditButton /> + </Datagrid> + </ReferenceManyField> + </FormTab> + </TabbedForm> + </Edit> +); +``` + +## Default Values + +To define default values, you can add a `defaultValue` prop to form components (`<SimpleForm>`, `<Tabbedform>`, etc.), or add a `defaultValue` to individual input components. Let's see each of these options. + +### Global Default Value + +The value of the form `defaultValue` prop can be an object, or a function returning an object, specifying default values for the created record. For instance: + +```jsx +const postDefaultValue = { created_at: new Date(), nb_views: 0 }; +export const PostCreate = (props) => ( + <Create {...props}> + <SimpleForm defaultValue={postDefaultValue}> + <TextInput source="title" /> + <RichTextInput source="body" /> + <NumberInput source="nb_views" /> + </SimpleForm> + </Create> +); +``` + +**Tip**: You can include properties in the form `defaultValue` that are not listed as input components, like the `created_at` property in the previous example. + +### Per Input Default Value + +Alternatively, you can specify a `defaultValue` prop directly in `<Input>` components. Just like for form-level default values, an input-level default value can be a scalar, or a function returning a scalar. React-admin will merge the input default values with the form default value (input > form): + +```jsx +export const PostCreate = (props) => ( + <Create {...props}> + <SimpleForm> + <DisabledInput source="id" defaultValue={() => uuid()}/> + <TextInput source="title" /> + <RichTextInput source="body" /> + <NumberInput source="nb_views" defaultValue={0} /> + </SimpleForm> + </Create> +); +``` + +## Validation + +React-admin relies on [redux-form](http://redux-form.com/) for the validation. + +To validate values submitted by a form, you can add a `validate` prop to the form component, to individual inputs, or even mix both approaches. + +### Global Validation + +The value of the form `validate` prop must be a function taking the record as input, and returning an object with error messages indexed by field. For instance: + +```jsx +const validateUserCreation = (values) => { + const errors = {}; + if (!values.firstName) { + errors.firstName = ['The firstName is required']; + } + if (!values.age) { + errors.age = ['The age is required']; + } else if (values.age < 18) { + errors.age = ['Must be over 18']; + } + return errors +}; + +export const UserCreate = (props) => ( + <Create {...props}> + <SimpleForm validate={validateUserCreation}> + <TextInput label="First Name" source="firstName" /> + <TextInput label="Age" source="age" /> + </SimpleForm> + </Create> +); +``` + +**Tip**: The props you pass to `<SimpleForm>` and `<TabbedForm>` end up as `reduxForm()` parameters. This means that, in addition to `validate`, you can also pass `warn` or `asyncValidate` functions. Read the [`reduxForm()` documentation](http://redux-form.com/6.5.0/docs/api/ReduxForm.md/) for details. + +### Per Input Validation: Function Validator + +Alternatively, you can specify a `validate` prop directly in `<Input>` components, taking either a function, or an array of functions. These functions should return `undefined` when there is no error, or an error string. + +```jsx +const required = (message = 'Required') => + value => value ? undefined : message; +const maxLength = (max, message = 'Too short') => + value => value && value.length > max ? message : undefined; +const number = (message = 'Must be a number') => + value => value && isNaN(Number(value)) ? message : undefined; +const minValue = (min, message = 'Too small') => + value => value && value < min ? message : undefined; + +const ageValidation = (value, allValues) => { + if (!value) { + return 'The age is required'; + } + if (value < 18) { + return 'Must be over 18'; + } + return []; +} + +const validateFirstName = [required(), maxLength(15)]; +const validateAge = [required(), number(), ageValidation]; + +export const UserCreate = (props) => ( + <Create {...props}> + <SimpleForm> + <TextInput label="First Name" source="firstName" validate={validateFirstName} /> + <TextInput label="Age" source="age" validate={validateAge}/> + </SimpleForm> + </Create> +); +``` + +React-admin will combine all the input-level functions into a single function looking just like the previous one. + +Input validation functions receive the current field value, and the values of all fields of the current record. This allows for complex validation scenarios (e.g. validate that two passwords are the same). + +**Tip**: Validator functions receive the form `props` as third parameter, including the `translate` function. This lets you build internationalized validators: + +```jsx +const required = (message = 'myroot.validation.required') => + (value, allValues, props) => value ? undefined : props.translate(message); +``` + +**Tip**: Make sure to define validation functions or array of functions in a variable, instead of defining them directly in JSX. This can result in a new function or array at every render, and trigger infinite rerender. + +```jsx +const validateStock = [required(), number(), minValue(0)]; + +export const ProductEdit = ({ ...props }) => ( + <Edit {...props}> + <SimpleForm defaultValue={{ stock: 0 }}> + ... + {/* do this */} + <NumberInput source="stock" validate={validateStock} /> + {/* don't do that */} + <NumberInput source="stock" validate={[required(), number(), minValue(0)]} /> + ... + </SimpleForm> + </Edit> +); +``` + +**Tip**: The props of your Input components are passed to a redux-form `<Field>` component. So in addition to `validate`, you can also use `warn`. + +**Tip**: You can use *both* Form validation and input validation. + +### Built-in Field Validators + +React-admin already bundles a few validator functions, that you can just require, and use as input-level validators: + +* `required(message)` if the field is mandatory, +* `minValue(min, message)` to specify a minimum value for integers, +* `maxValue(max, message)` to specify a maximum value for integers, +* `minLength(min, message)` to specify a minimum length for strings, +* `maxLength(max, message)` to specify a maximum length for strings, +* `number(message)` to check that the input is a valid number, +* `email(message)` to check that the input is a valid email address, +* `regex(pattern, message)` to validate that the input matches a regex, +* `choices(list, message)` to validate that the input is within a given list, + +Example usage: + +```jsx +import { + required, + minLength, + maxLength, + minValue, + maxValue, + number, + regex, + email, + choices +} from 'react-admin'; + +const validateFirstName = [required(), minLength(2), maxLength(15)]; +const validateEmail = email(); +const validateAge = [number(), minValue(18)]; +const validateZipCode = regex(/^\d{5}$/, 'Must be a valid Zip Code'); +const validateSex = choices(['m', 'f'], 'Must be Male or Female'); + +export const UserCreate = (props) => ( + <Create {...props}> + <SimpleForm> + <TextInput label="First Name" source="firstName" validate={validateFirstName} /> + <TextInput label="Email" source="email" validate={validateEmail} /> + <TextInput label="Age" source="age" validate={validateAge}/> + <TextInput label="Zip Code" source="zip" validate={validateZipCode}/> + <SelectInput label="Sex" source="sex" choices={[ + { id: 'm', name: 'Male' }, + { id: 'f', name: 'Female' }, + ]} validate={validateSex}/> + </SimpleForm> + </Create> +); +``` + +**Tip**: If you pass a function as a message, react-admin calls this function with `{ args, value, values,translate, ...props }` as argument. For instance: + +```jsx +const message = ({ translate }) => translate('myroot.validation.email_invalid'); +const validateEmail = email(message); +``` + +## Submit On Enter + +By default, pressing `ENTER` in any of the form fields submits the form - this is the expected behavior in most cases. However, some of your custom input components (e.g. Google Maps widget) may have special handlers for the `ENTER` key. In that case, to disable the automated form submission on enter, set the `submitOnEnter` prop of the form component to `false`: + +```jsx +export const PostEdit = (props) => ( + <Edit {...props}> + <SimpleForm submitOnEnter={false}> + ... + </SimpleForm> + </Edit> +); +``` + +## Redirection After Submission + +By default: + +* Submitting the form in the `<Create>` view redirects to the `<Edit>` view +* Submitting the form in the `<Edit>` view redirects to the `<List>` view + +You can customize the redirection by setting the `redirect` prop of the form component. Possible values are "edit", "show", "list", and `false` to disable redirection. You may also specify a custom path such as `/my-custom-route`. For instance, to redirect to the `<Show>` view after edition: + +```jsx +export const PostEdit = (props) => ( + <Edit {...props}> + <SimpleForm redirect="show"> + ... + </SimpleForm> + </Edit> +); +``` + +You can also pass a custom route (e.g. "/home") or a function as `redirect` prop value. For example, if you want to redirect to a page related to the current object: + +```jsx +// redirect to the related Author show page +const redirect = (basePath, id, data) => `/author/${data.author_id}/show`; + +export const PostEdit = (props) => { + <Edit {...props}> + <SimpleForm redirect={redirect}> + ... + </SimpleForm> + </Edit> +); +``` + +This affects both the submit button, and the form submission when the user presses `ENTER` in one of the form fields. + +## Toolbar + +At the bottom of the form, the toolbar displays the submit button. You can override this component by setting the `toolbar` prop, to display the buttons of your choice. + +The most common use case is to display two submit buttons in the `<Create>` view: + +* one that creates and redirects to the `<Show>` view of the new resource, and +* one that redirects to a blank `<Create>` view after creation (allowing bulk creation) + +![Form toolbar](https://marmelab.com/react-admin/img/form-toolbar.png) + +For that use case, use the `<SaveButton>` component with a custom `redirect` prop: + +```jsx +import { Edit, SimpleForm, SaveButton, Toolbar } from 'react-admin'; + +const PostCreateToolbar = props => ( + <Toolbar {...props} > + <SaveButton + label="post.action.save_and_show" + redirect="show" + submitOnEnter={true} + /> + <SaveButton + label="post.action.save_and_add" + redirect={false} + submitOnEnter={false} + variant="flat" + /> + </Toolbar> +); + +export const PostEdit = (props) => ( + <Edit {...props}> + <SimpleForm toolbar={<PostCreateToolbar />} redirect="show"> + ... + </SimpleForm> + </Edit> +); +``` + +Here are the props received by the `Toolbar` component when passed as the `toolbar` prop of the `SimpleForm` or `TabbedForm` components: + +* `handleSubmitWithRedirect`: The function to call in order to submit the form. It accepts a single parameter overriding the form's default redirect. +* `invalid`: A boolean indicating whether the form is invalid +* `pristine`: A boolean indicating whether the form is pristine (eg: no inputs have been changed yet) +* `redirect`: The default form's redirect +* `saving`: A boolean indicating whether a save operation is ongoing. +* `submitOnEnter`: A boolean indicating whether the form should be submitted when pressing `enter` + +**Tip**: Use react-admin's `<Toolbar>` component instead of material-ui's `<Toolbar>` component. The former builds up on the latter, and adds support for an alternative mobile layout (and is therefore responsive). + +**Tip**: Don't forget to also set the `redirect` prop of the Form component to handle submission by the `ENTER` key. + +## Customizing Input Container Styles + +The input components are wrapped inside a `div` to ensure a good looking form by default. You can pass a `formClassName` prop to the input components to customize the style of this `div`. For example, here is how to display two inputs on the same line: + +```jsx +const styles = { + inlineBlock: { display: 'inline-flex', marginRight: '1rem' }, +}; +export const UserEdit = withStyles(styles)(({ classes, ...props }) => ( + <Edit {...props}> + <SimpleForm> + <TextInput source="first_name" formClassName={classes.inlineBlock} /> + <TextInput source="last_name" formClassName={classes.inlineBlock} /> + {/* This input will be display below the two first ones */} + <TextInput source="email" type="email" /> + </SimpleForm> + </Edit> +``` + +## Displaying Fields or Inputs depending on the user permissions + +You might want to display some fields, inputs or filters only to users with specific permissions. Those permissions are retrieved for each route and will provided to your component as a `permissions` prop. + +Each route will call the `authProvider` with the `AUTH_GET_PERMISSIONS` type and some parameters including the current location and route parameters. It's up to you to return whatever you need to check inside your component such as the user's role, etc. + +Here's an example inside a `Create` view with a `SimpleForm` and a custom `Toolbar`: + +```jsx +const UserCreateToolbar = ({ permissions, ...props }) => + <Toolbar {...props}> + <SaveButton + label="user.action.save_and_show" + redirect="show" + submitOnEnter={true} + /> + {permissions === 'admin' && + <SaveButton + label="user.action.save_and_add" + redirect={false} + submitOnEnter={false} + variant="flat" + />} + </Toolbar>; + +export const UserCreate = ({ permissions, ...props }) => + <Create {...props}> + <SimpleForm + toolbar={<UserCreateToolbar permissions={permissions} />} + defaultValue={{ role: 'user' }} + > + <TextInput source="name" validate={[required()]} /> + {permissions === 'admin' && + <TextInput source="role" validate={[required()]} />} + </SimpleForm> + </Create>; +``` + +**Tip**: Note how the `permissions` prop is passed down to the custom `toolbar` component. + +This also works inside an `Edition` view with a `TabbedForm`, and you can hide a `FormTab` completely: + +```jsx +export const UserEdit = ({ permissions, ...props }) => + <Edit title={<UserTitle />} {...props}> + <TabbedForm defaultValue={{ role: 'user' }}> + <FormTab label="user.form.summary"> + {permissions === 'admin' && <DisabledInput source="id" />} + <TextInput source="name" validate={required()} /> + </FormTab> + {permissions === 'admin' && + <FormTab label="user.form.security"> + <TextInput source="role" validate={required()} /> + </FormTab>} + </TabbedForm> + </Edit>; +``` \ No newline at end of file From 34515ac5a459cef13a7b636c5cb76c2a7f2c8f10 Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:19 +0800 Subject: [PATCH 18/20] New translations authorization.md (Chinese Simplified) --- .../translated_docs/zh-CN/authorization.md | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 website/translated_docs/zh-CN/authorization.md diff --git a/website/translated_docs/zh-CN/authorization.md b/website/translated_docs/zh-CN/authorization.md new file mode 100644 index 0000000..9914c24 --- /dev/null +++ b/website/translated_docs/zh-CN/authorization.md @@ -0,0 +1,286 @@ +--- +id: authorization +title: Authorization +--- +Some applications may require to determine what level of access a particular authenticated user should have to secured resources. Since there are many different possible strategies (single role, multiple roles or rights, etc.), react-admin simply provides hooks to execute your own authorization code. + +By default, a react-admin app doesn't require authorization. However, if needed, it will rely on the `authProvider` introduced in the [Authentication](./Authentication.html) section. + +## Configuring the Auth Provider + +A call to the `authProvider` with the `AUTH_GET_PERMISSIONS` type will be made each time a component requires to check the user's permissions. + +Following is an example where the `authProvider` stores the user's role upon authentication, and returns it when called for a permissions check: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_GET_PERMISSIONS } from 'react-admin'; +import decodeJwt from 'jwt-decode'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + const { username, password } = params; + const request = new Request('https://mydomain.com/authenticate', { + method: 'POST', + body: JSON.stringify({ username, password }), + headers: new Headers({ 'Content-Type': 'application/json' }), + }) + return fetch(request) + .then(response => { + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(({ token }) => { + const decodedToken = decodeJwt(token); + localStorage.setItem('token', token); + localStorage.setItem('role', decodedToken.role); + }); + } + if (type === AUTH_LOGOUT) { + localStorage.removeItem('token'); + localStorage.removeItem('role'); + return Promise.resolve(); + } + if (type === AUTH_ERROR) { + // ... + } + if (type === AUTH_CHECK) { + return localStorage.getItem('token') ? Promise.resolve() : Promise.reject(); + } + if (type === AUTH_GET_PERMISSIONS) { + const role = localStorage.getItem('role'); + return role ? Promise.resolve(role) : Promise.reject(); + } + return Promise.reject('Unkown method'); +}; +``` + +## Restricting Access to Resources or Views + +It's possible to restrict access to resources or their views inside the `Admin` component. To do so, you must specify a function as the `Admin` only child. This function will be called with the permissions returned by the `authProvider`. + +```jsx +<Admin + dataProvider={dataProvider} + authProvider={authProvider} +> + {permissions => [ + // Restrict access to the edit and remove views to admin only + <Resource + name="customers" + list={VisitorList} + edit={permissions === 'admin' ? VisitorEdit : null} + icon={VisitorIcon} + />, + // Only include the categories resource for admin users + permissions === 'admin' + ? <Resource name="categories" list={CategoryList} edit={CategoryEdit} icon={CategoryIcon} /> + : null, + ]} +</Admin> +``` + +Note that the function returns an array of React elements. This is required to avoid having to wrap them in a container element which would prevent the `Admin` from working. + +**Tip** Even if that's possible, be careful when completely excluding a resource (like with the `categories` resource in this example) as it will prevent you to reference them in the other resource views, too. + +## Restricting Access to Fields and Inputs + +You might want to display some fields or inputs only to users with specific permissions. Those permissions are retrieved for each route and will provided to your component as a `permissions` prop. + +Each route will call the `authProvider` with the `AUTH_GET_PERMISSIONS` type and some parameters including the current location and route parameters. It's up to you to return whatever you need to check inside your component such as the user's role, etc. + +Here's an example inside a `Create` view with a `SimpleForm` and a custom `Toolbar`: + +```jsx +const UserCreateToolbar = ({ permissions, ...props }) => + <Toolbar {...props}> + <SaveButton + label="user.action.save_and_show" + redirect="show" + submitOnEnter={true} + /> + {permissions === 'admin' && + <SaveButton + label="user.action.save_and_add" + redirect={false} + submitOnEnter={false} + variant="flat" + />} + </Toolbar>; + +export const UserCreate = ({ permissions, ...props }) => + <Create {...props}> + <SimpleForm + toolbar={<UserCreateToolbar permissions={permissions} />} + defaultValue={{ role: 'user' }} + > + <TextInput source="name" validate={[required()]} /> + {permissions === 'admin' && + <TextInput source="role" validate={[required()]} />} + </SimpleForm> + </Create>; +``` + +**Tip** Note how the `permissions` prop is passed down to the custom `toolbar` component. + +This also works inside an `Edition` view with a `TabbedForm`, and you can hide a `FormTab` completely: + +```jsx +export const UserEdit = ({ permissions, ...props }) => + <Edit title={<UserTitle />} {...props}> + <TabbedForm defaultValue={{ role: 'user' }}> + <FormTab label="user.form.summary"> + {permissions === 'admin' && <DisabledInput source="id" />} + <TextInput source="name" validate={required()} /> + </FormTab> + {permissions === 'admin' && + <FormTab label="user.form.security"> + <TextInput source="role" validate={required()} /> + </FormTab>} + </TabbedForm> + </Edit>; +``` + +What about the `List` view, the `DataGrid`, `SimpleList` and `Filter` components? It works there, too. + +```jsx +const UserFilter = ({ permissions, ...props }) => + <Filter {...props}> + <TextInput + label="user.list.search" + source="q" + alwaysOn + /> + <TextInput source="name" /> + {permissions === 'admin' ? <TextInput source="role" /> : null} + </Filter>; + +export const UserList = ({ permissions, ...props }) => + <List + {...props} + filters={<UserFilter permissions={permissions} />} + sort={{ field: 'name', order: 'ASC' }} + > + <Responsive + small={ + <SimpleList + primaryText={record => record.name} + secondaryText={record => + permissions === 'admin' ? record.role : null} + /> + } + medium={ + <Datagrid> + <TextField source="id" /> + <TextField source="name" /> + {permissions === 'admin' && <TextField source="role" />} + {permissions === 'admin' && <EditButton />} + <ShowButton /> + </Datagrid> + } + /> + </List>; +``` + +**Tip** Note how the `permissions` prop is passed down to the custom `filters` component. + +## Restricting Access to Content Inside a Dashboard + +The component provided as a [`dashboard`]('./Admin.md#dashboard) will receive the permissions in its props too: + +```jsx +// in src/Dashboard.js +import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { ViewTitle } from 'react-admin'; + +export default ({ permissions }) => ( + <Card> + <ViewTitle title="Dashboard" /> + <CardContent>Lorem ipsum sic dolor amet...</CardContent> + {permissions === 'admin' + ? <CardContent>Sensitive data</CardContent> + : null + } + </Card> +); +``` + +## Restricting Access to Content Inside Custom Pages + +You might want to check user permissions inside a [custom pages](./Admin.md#customroutes). You'll have to use the `WithPermissions` component for that. It will ensure the user is authenticated then call the `authProvider` with the `AUTH_GET_PERMISSIONS` type and the `authParams` you specify: + +```jsx +// in src/MyPage.js +import React from 'react'; +import Card from '@material-ui/core/Card'; +import CardContent from '@material-ui/core/CardContent'; +import { ViewTitle, WithPermissions } from 'react-admin'; +import { withRouter } from 'react-router-dom'; + +const MyPage = ({ permissions }) => ( + <Card> + <ViewTitle title="My custom page" /> + <CardContent>Lorem ipsum sic dolor amet...</CardContent> + {permissions === 'admin' + ? <CardContent>Sensitive data</CardContent> + : null + } + </Card> +) +const MyPageWithPermissions = ({ location, match }) => ( + <WithPermissions + authParams={{ key: match.path, params: route.params }} + // location is not required but it will trigger a new permissions check if specified when it changes + location={location} + render={({ permissions }) => <MyPage permissions={permissions} /> } + /> +); + +export default MyPageWithPermissions; + +// in src/customRoutes.js +import React from 'react'; +import { Route } from 'react-router-dom'; +import Foo from './Foo'; +import Bar from './Bar'; +import Baz from './Baz'; +import MyPageWithPermissions from './MyPage'; + +export default [ + <Route exact path="/foo" component={Foo} />, + <Route exact path="/bar" component={Bar} />, + <Route exact path="/baz" component={Baz} noLayout />, + <Route exact path="/baz" component={MyPageWithPermissions} />, +]; +``` + +## Restricting Access to Content in Custom Menu + +What if you want to check the permissions inside a [custom menu](./Admin.html#menu) ? Much like getting permissions inside a custom page, you'll have to use the `WithPermissions` component: + +```jsx +// in src/myMenu.js +import React from 'react'; +import { connect } from 'react-redux'; +import { MenuItemLink, WithPermissions } from 'react-admin'; + +const Menu = ({ onMenuClick, logout, permissions }) => ( + <div> + <MenuItemLink to="/posts" primaryText="Posts" onClick={onMenuClick} /> + <MenuItemLink to="/comments" primaryText="Comments" onClick={onMenuClick} /> + <WithPermissions + render={({ permissions }) => ( + permissions === 'admin' + ? <MenuItemLink to="/custom-route" primaryText="Miscellaneous" onClick={onMenuClick} /> + : null + )} + /> + {logout} + </div> +); +``` \ No newline at end of file From b76dd01c131a990922ae7c4c8ea22fb2eac1c38a Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:20 +0800 Subject: [PATCH 19/20] New translations authentication.md (Chinese Simplified) --- .../translated_docs/zh-CN/authentication.md | 344 ++++++++++++++++++ 1 file changed, 344 insertions(+) create mode 100644 website/translated_docs/zh-CN/authentication.md diff --git a/website/translated_docs/zh-CN/authentication.md b/website/translated_docs/zh-CN/authentication.md new file mode 100644 index 0000000..da40e29 --- /dev/null +++ b/website/translated_docs/zh-CN/authentication.md @@ -0,0 +1,344 @@ +--- +id: authentication +title: Authentication +--- +![Logout button](https://marmelab.com/react-admin/img/login.gif) + +React-admin lets you secure your admin app with the authentication strategy of your choice. Since there are many different possible strategies (Basic Auth, JWT, OAuth, etc.), react-admin simply provides hooks to execute your own authentication code. + +By default, an react-admin app doesn't require authentication. But if the REST API ever returns a 401 (Unauthorized) or a 403 (Forbidden) response, then the user is redirected to the `/login` route. You have nothing to do - it's already built in. + +## Configuring the Auth Provider + +By default, the `/login` route renders a special component called `Login`, which displays a login form asking for username and password. + +![Default Login Form](https://marmelab.com/react-admin/img/login-form.png) + +What this form does upon submission depends on the `authProvider` prop of the `<Admin>` component. This function receives authentication requests `(type, params)`, and should return a Promise. `Login` calls `authProvider` with the `AUTH_LOGIN` type, and `{ login, password }` as parameters. It's the ideal place to authenticate the user, and store their credentials. + +For instance, to query an authentication route via HTTPS and store the credentials (a token) in local storage, configure `authProvider` as follows: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN } from 'react-admin'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + const { username, password } = params; + const request = new Request('https://mydomain.com/authenticate', { + method: 'POST', + body: JSON.stringify({ username, password }), + headers: new Headers({ 'Content-Type': 'application/json' }), + }) + return fetch(request) + .then(response => { + if (response.status < 200 || response.status >= 300) { + throw new Error(response.statusText); + } + return response.json(); + }) + .then(({ token }) => { + localStorage.setItem('token', token); + }); + } + return Promise.resolve(); +} +``` + +**Tip**: It's a good idea to store credentials in `localStorage`, to avoid reconnection when opening a new browser tab. But this makes your application [open to XSS attacks](http://www.redotheweb.com/2015/11/09/api-security.html), so you'd better double down on security, and add an `httpOnly` cookie on the server side, too. + +Then, pass this client to the `<Admin>` component: + +```jsx +// in src/App.js +import authProvider from './authProvider'; + +const App = () => ( + <Admin authProvider={authProvider}> + ... + </Admin> +); +``` + +Upon receiving a 403 response, the admin app shows the Login page. `authProvider` is now called when the user submits the login form. Once the promise resolves, the login form redirects to the previous page, or to the admin index if the user just arrived. + +## Sending Credentials to the API + +To use the credentials when calling a data provider, you have to tweak, this time, the `dataProvider` function. As explained in the [Data providers documentation](DataProviders.md#adding-custom-headers), `simpleRestProvider` and `jsonServerProvider` take an `httpClient` as second parameter. That's the place where you can change request headers, cookies, etc. + +For instance, to pass the token obtained during login as an `Authorization` header, configure the Data Provider as follows: + +```jsx +import { fetchUtils, Admin, Resource } from 'react-admin'; +import simpleRestProvider from 'ra-data-simple-rest'; + +const httpClient = (url, options = {}) => { + if (!options.headers) { + options.headers = new Headers({ Accept: 'application/json' }); + } + const token = localStorage.getItem('token'); + options.headers.set('Authorization', `Bearer ${token}`); + return fetchUtils.fetchJson(url, options); +} +const dataProvider = simpleRestProvider('http://localhost:3000', httpClient); + +const App = () => ( + <Admin dataProvider={dataProvider} authProvider={authProvider}> + ... + </Admin> +); +``` + +If you have a custom REST client, don't forget to add credentials yourself. + +## Adding a Logout Button + +If you provide an `authProvider` prop to `<Admin>`, react-admin displays a logout button in the top bar (or in the menu on mobile). When the user clicks on the logout button, this calls the `authProvider` with the `AUTH_LOGOUT` type and removes potentially sensitive data from the redux store. When resolved, the user gets redirected to the login page. + +For instance, to remove the token from local storage upon logout: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT } from 'react-admin'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + // ... + } + if (type === AUTH_LOGOUT) { + localStorage.removeItem('token'); + return Promise.resolve(); + } + return Promise.resolve(); +}; +``` + +![Logout button](https://marmelab.com/react-admin/img/logout.gif) + +The `authProvider` is also a good place to notify the authentication API that the user credentials are no longer valid after logout. + +## Catching Authentication Errors On The API + +Even though a user may be authenticated on the client-side, their credentials may no longer be valid server-side (e.g. if the token is only valid for a couple weeks). In that case, the API usually answers to all REST requests with an error code 401 or 403 - but what about *your* API? + +Fortunately, each time the API returns an error, the `authProvider` is called with the `AUTH_ERROR` type. Once again, it's up to you to decide which HTTP status codes should let the user continue (by returning a resolved promise) or log them out (by returning a rejected promise). + +For instance, to redirect the user to the login page for both 401 and 403 codes: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR } from 'react-admin'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + // ... + } + if (type === AUTH_LOGOUT) { + // ... + } + if (type === AUTH_ERROR) { + const status = params.status; + if (status === 401 || status === 403) { + localStorage.removeItem('token'); + return Promise.reject(); + } + return Promise.resolve(); + } + return Promise.resolve(); +}; +``` + +## Checking Credentials During Navigation + +Redirecting to the login page whenever a REST response uses a 401 status code is usually not enough, because react-admin keeps data on the client side, and could display stale data while contacting the server - even after the credentials are no longer valid. + +Fortunately, each time the user navigates, react-admin calls the `authProvider` with the `AUTH_CHECK` type, so it's the ideal place to check for credentials. + +For instance, to check for the existence of the token in local storage: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + // ... + } + if (type === AUTH_LOGOUT) { + // ... + } + if (type === AUTH_ERROR) { + // ... + } + if (type === AUTH_CHECK) { + return localStorage.getItem('token') ? Promise.resolve() : Promise.reject(); + } + return Promise.reject('Unkown method'); +}; +``` + +If the promise is rejected, react-admin redirects by default to the `/login` page. You can override where to redirect the user by passing an argument with a `redirectTo` property to the rejected promise: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + // ... + } + if (type === AUTH_LOGOUT) { + // ... + } + if (type === AUTH_ERROR) { + // ... + } + if (type === AUTH_CHECK) { + return localStorage.getItem('token') ? Promise.resolve() : Promise.reject({ redirectTo: '/no-access' }); + } + return Promise.reject('Unkown method'); +}; +``` + +**Tip**: For the `AUTH_CHECK` call, the `params` argument contains the `resource` name, so you can implement different checks for different resources: + +```jsx +// in src/authProvider.js +import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin'; + +export default (type, params) => { + if (type === AUTH_LOGIN) { + // ... + } + if (type === AUTH_LOGOUT) { + // ... + } + if (type === AUTH_ERROR) { + // ... + } + if (type === AUTH_CHECK) { + const { resource } = params; + if (resource === 'posts') { + // check credentials for the posts resource + } + if (resource === 'comments') { + // check credentials for the comments resource + } + } + return Promise.reject('Unkown method'); +}; +``` + +**Tip**: The `authProvider` can only be called with `AUTH_LOGIN`, `AUTH_LOGOUT`, `AUTH_ERROR`, or `AUTH_CHECK`; that's why the final return is a rejected promise. + +## Customizing The Login and Logout Components + +Using `authProvider` and `checkCredentials` is enough to implement a full-featured authorization system if the authentication relies on a username and password. + +But what if you want to use an email instead of a username? What if you want to use a Single-Sign-On (SSO) with a third-party authentication service? What if you want to use two-factor authentication? + +For all these cases, it's up to you to implement your own `LoginPage` component, which will be displayed under the `/login` route instead of the default username/password form, and your own `LogoutButton` component, which will be displayed in the sidebar. Pass both these components to the `<Admin>` component: + +**Tip**: Use the `userLogin` and `userLogout` actions in your custom `Login` and `Logout` components. + +```jsx +// in src/MyLoginPage.js +import React, { Component } from 'react'; +import { connect } from 'react-redux'; +import { userLogin } from 'react-admin'; + +class MyLoginPage extends Component { + submit = (e) => { + e.preventDefault(); + // gather your data/credentials here + const credentials = { }; + + // Dispatch the userLogin action (injected by connect) + this.props.userLogin(credentials); + } + + render() { + return ( + <form onSubmit={this.submit}> + ... + </form> + ); + } +}; + +export default connect(undefined, { userLogin })(MyLoginPage); + +// in src/MyLogoutButton.js +import React from 'react'; +import { connect } from 'react-redux'; +import { Responsive, userLogout } from 'react-admin'; +import MenuItem from '@material-ui/core/MenuItem'; +import Button from '@material-ui/core/Button'; +import ExitIcon from '@material-ui/icons/PowerSettingsNew'; + +const MyLogoutButton = ({ userLogout, ...rest }) => ( + <Responsive + xsmall={ + <MenuItem + onClick={userLogout} + {...sanitizeRestProps(rest)} + > + <ExitIcon /> Logout + </MenuItem> + } + medium={ + <Button + onClick={userLogout} + size="small" + {...sanitizeRestProps(rest)} + > + <ExitIcon /> Logout + </Button> + } + /> +); +export default connect(undefined, { userLogout: userLogout() })(MyLogoutButton); + +// in src/App.js +import MyLoginPage from './MyLoginPage'; +import MyLogoutButton from './MyLogoutButton'; + +const App = () => ( + <Admin loginPage={MyLoginPage} logoutButton={MyLogoutButton} authProvider={authProvider}> + ... + </Admin> +); +``` + +## Restricting Access To A Custom Page + +If you add [custom pages](./Actions.md), of if you [create an admin app from scratch](./CustomApp.md), you may need to secure access to pages manually. That's the purpose of the `<Authenticated>` component, that you can use as a decorator for your own components. + +```jsx +// in src/MyPage.js +import { withRouter } from 'react-router-dom'; +import { Authenticated } from 'react-admin'; + +const MyPage = ({ location }) => ( + <Authenticated authParams={{ foo: 'bar' }} location={location}> + <div> + ... + </div> + </Authenticated> +); + +export default withRouter(MyPage); +``` + +The `<Authenticated>` component calls the `authProvider` function with `AUTH_CHECK` and `authParams`. If the response is a fulfilled promise, the child component is rendered. If the response is a rejected promise, `<Authenticated>` redirects to the login form. Upon successful login, the user is redirected to the initial location (that's why it's necessary to get the location from the router). + +## Redirect After Logout + +By default, react-admin redirects the user to '/login' after they log out. This can be changed by passing the url to redirect to as parameter to the `userLogout()` action creator when you `connect` the `MyLogoutButton` component: + +```diff +// in src/MyLogoutButton.js +// ... +- export default connect(undefined, { userLogout: userLogout() })(MyLogoutButton); ++ export default connect(undefined, { userLogout: userLogout('/') })(MyLogoutButton); +``` \ No newline at end of file From 1d0203cdc9c57c64c4e887112a4f1b441685308e Mon Sep 17 00:00:00 2001 From: Kirk <kirk.w.wang@gmail.com> Date: Thu, 19 Jul 2018 23:15:21 +0800 Subject: [PATCH 20/20] New translations en.json (Chinese Simplified) --- website/i18n/zh-CN.json | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 website/i18n/zh-CN.json diff --git a/website/i18n/zh-CN.json b/website/i18n/zh-CN.json new file mode 100644 index 0000000..a7997ec --- /dev/null +++ b/website/i18n/zh-CN.json @@ -0,0 +1,41 @@ +{ + "_comment": "This file is auto-generated by write-translations.js", + "localized-strings": { + "next": "Next", + "previous": "Previous", + "tagline": "A frontend framework for building admin SPAs on top of REST services, using React and Material Design", + "actions": "Writing Actions", + "admin-component": "<Admin>", + "authentication": "Authentication", + "authorization": "Authorization", + "creat-edit-view-components": "<Create> and <Edit> Views", + "custom-app": "Including the Admin in Another App", + "data-providers": "Data Providers", + "ecosystem": "Ecosystem", + "faq": "FAQ", + "field-components": "<Field> Components", + "input-components": "<Input> components", + "intro": "Introduction", + "list-view-component": "<List> View", + "reference": "Reference", + "resource-component": "<Resource>", + "show-view-component": "<Show> View", + "theming": "Theming", + "translation": "Translation", + "tutorial": "Tutorial", + "Doc": "Doc", + "Knowledge": "Knowledge" + }, + "pages-strings": { + "Need help?|no description given": "Need help?", + "This project is maintained by a dedicated group of people.|no description given": "This project is maintained by a dedicated group of people.", + "Docs|no description given": "Docs", + "Who's Using This?|no description given": "Who's Using This?", + "This project is used by many folks|no description given": "This project is used by many folks", + "Are you using this project?|no description given": "Are you using this project?", + "Add your profile|no description given": "Add your profile", + "Help Translate|recruit community translators for your project": "Help Translate", + "Edit this Doc|recruitment message asking to edit the doc source": "Edit", + "Translate this Doc|recruitment message asking to translate the docs": "Translate" + } +} \ No newline at end of file