mirror of
https://github.com/strapi/strapi.git
synced 2025-12-25 22:23:10 +00:00
Delete plugins doc folder
This commit is contained in:
parent
95cb588750
commit
ed198423d6
@ -1,827 +0,0 @@
|
||||
# Plugin advanced usage
|
||||
|
||||
This section contains advanced resources to develop plugins.
|
||||
|
||||
## Handle user navigation
|
||||
|
||||
User navigation within your plugin can be managed by two different ways :
|
||||
- Using the [React Router V4 API](https://reacttraining.com/react-router/web/guides/philosophy)
|
||||
- Using the main router from the app
|
||||
|
||||
### Using React Router
|
||||
|
||||
[Link](https://reacttraining.com/react-router/web/api/Link) provides declarative, accessible navigation around your application :
|
||||
|
||||
```js
|
||||
<Link to={{
|
||||
pathname: `/plugins/my-plugin/foo/${this.props.bar}`,
|
||||
search: '?foo=bar',
|
||||
hash: '#the-hash',
|
||||
}} />
|
||||
|
||||
// Same as
|
||||
|
||||
<Link to=`/plugins/my-plugin/foo/${this.props.bar}?foo=bar#the-hash` />
|
||||
```
|
||||
|
||||
[NavLink](https://reacttraining.com/react-router/web/api/NavLink) will add styling attributes to the rendered element when it matches the current URL.
|
||||
|
||||
|
||||
```js
|
||||
<NavLink
|
||||
to="/faq"
|
||||
activeClassName="selected"
|
||||
>
|
||||
FAQs
|
||||
</NavLink>
|
||||
```
|
||||
|
||||
### Using the App Router
|
||||
|
||||
We use the app router if we want to make a redirection after some user actions (ex: after submitting a form).
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, compose } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// App router
|
||||
import { router } from 'app';
|
||||
|
||||
// Components
|
||||
import Input from 'components/inputs';
|
||||
import Button from 'components/button';
|
||||
|
||||
// Utils
|
||||
import injectSaga from 'utils/injectSaga';
|
||||
import injectReducer from 'utils/injectReducer';
|
||||
|
||||
// Actions
|
||||
import { changeInput, submitForm } from './actions';
|
||||
// Sagas
|
||||
import saga from './sagas';
|
||||
// Selectors
|
||||
import selectFooPage from './selectors';
|
||||
// Reducer
|
||||
import reducer from './reducer';
|
||||
|
||||
export class FooPage extends React.Component {
|
||||
handleSubmit = () => {
|
||||
this.props.handleSubmit();
|
||||
const hash = this.props.location.hash;
|
||||
const pathname = this.props.match.pathname;
|
||||
const search = '?foo=bar';
|
||||
router.push({ pathname, search, hash });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={this.handleSubmit}>
|
||||
<Input
|
||||
value={this.state.value}
|
||||
handleChange={this.props.changeInput}
|
||||
validations={{ required: true }}
|
||||
label="Text field"
|
||||
target="data"
|
||||
type="text"
|
||||
/>
|
||||
<Button primary onClick={this.handleSubmit}>Submit form</Button>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
FooPage.propTypes = {
|
||||
changeInput: PropTypes.func.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
match: PropTypes.object.isRequired,
|
||||
submitForm: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = selectFooPage();
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
changeInput,
|
||||
submitForm,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
const withReducer = injectReducer({ key: 'fooPage', reducer });
|
||||
const withSagas = injectSaga({ key: 'fooPage', saga });
|
||||
|
||||
export default compose(
|
||||
withReducer,
|
||||
withSagas,
|
||||
withConnect,
|
||||
)(FooPage);
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Inject design
|
||||
|
||||
The `ExtendComponent` allows you to inject design from one plugin into another.
|
||||
|
||||
### Example
|
||||
|
||||
Let's say that you want to enable another plugin to inject a component into the top area of your plugin's container called `FooPage`;
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/actions.js`.
|
||||
```js
|
||||
import {
|
||||
ON_TOGGLE_SHOW_LOREM,
|
||||
} from './constants';
|
||||
|
||||
export function onToggleShowLorem() {
|
||||
return {
|
||||
type: ON_TOGGLE_SHOW_LOREM,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Import the ExtendComponent
|
||||
import ExtendComponent from 'components/ExtendComponent';
|
||||
|
||||
// Utils
|
||||
import injectReducer from 'utils/injectReducer';
|
||||
|
||||
// Actions
|
||||
import { onToggleShowLorem } from './action'
|
||||
|
||||
import reducer from './reducer';
|
||||
|
||||
// Selectors
|
||||
import { makeSelectShowLorem } from './selectors';
|
||||
|
||||
class FooPage extends React.Component {
|
||||
render() {
|
||||
const lorem = this.props.showLorem ? <p>Lorem ipsum dolor sit amet, consectetur adipiscing</p> : '';
|
||||
return (
|
||||
<div>
|
||||
<h1>This is FooPage container</h1>
|
||||
<ExtendComponent
|
||||
area="top"
|
||||
container="FooPage"
|
||||
plugin="my-plugin"
|
||||
{...props}
|
||||
/>
|
||||
{lorem}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FooPage.propTypes = {
|
||||
onToggleShowLorem: PropTypes.func.isRequired,
|
||||
showLorem: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
onToggleShowLorem,
|
||||
},
|
||||
dispatch,
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
showLorem: makeSelectShowLorem(),
|
||||
});
|
||||
|
||||
const withConnect = connect(mapDispatchToProps, mapDispatchToProps);
|
||||
const withReducer = injectReducer({ key: 'fooPage', reducer });
|
||||
|
||||
export default compose(
|
||||
withReducer,
|
||||
withConnect,
|
||||
)(FooPage);
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/reducer.js`.
|
||||
```js
|
||||
import { fromJS } from 'immutable';
|
||||
import { ON_TOGGLE_SHOW_LOREM } from './constants';
|
||||
|
||||
const initialState = fromJS({
|
||||
showLorem: false,
|
||||
});
|
||||
|
||||
function fooPageReducer(state= initialState, action) {
|
||||
switch (action.type) {
|
||||
case ON_TOGGLE_SHOW_LOREM:
|
||||
return state.set('showLorem', !state.get('showLorem'));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default fooPageReducer;
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/selectors.js`.
|
||||
```js
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
/**
|
||||
* Direct selector to the fooPage state domain
|
||||
*/
|
||||
|
||||
const selectFooPageDomain = () => state => state.get('fooPage');
|
||||
|
||||
/**
|
||||
* Other specific selectors
|
||||
*/
|
||||
|
||||
const makeSelectShowLorem = () => createSelector(
|
||||
selectFooPageDomain(),
|
||||
(substate) => substate.get('showLorem'),
|
||||
);
|
||||
|
||||
export { makeSelectShowLorem };
|
||||
```
|
||||
|
||||
That's all now your plugin's container is injectable!
|
||||
|
||||
Let's see how to inject some design from another plugin.
|
||||
|
||||
|
||||
### Create your injectedComponent
|
||||
**Path -** `./plugins/another-plugin/admin/src/extendables/BarContainer/index.js`;
|
||||
```js
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Import our Button component
|
||||
import Button from 'components/Button';
|
||||
|
||||
// Other imports such as actions, selectors, sagas, reducer...
|
||||
|
||||
class BarContainer extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Button primary onClick={this.props.onToggleShowLorem}>
|
||||
Click me to show lorem paragraph
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
BarContainer.propTypes = {
|
||||
onToggleShowLorem: PropTypes.func,
|
||||
};
|
||||
|
||||
BarContainer.defaultProps = {
|
||||
onToggleShowLorem: () => {},
|
||||
};
|
||||
|
||||
export default BarContainer;
|
||||
```
|
||||
|
||||
### Tell the admin that you want to inject some design into another plugin
|
||||
|
||||
You have to create a file called `injectedComponents.js` at the root of your `another-plugin` src folder.
|
||||
|
||||
**Path —** `./plugins/another-plugin/admin/src/injectedComponents.js`.
|
||||
```js
|
||||
import BarContainer from 'extendables/BarContainer';
|
||||
|
||||
// export an array containing all the injected components
|
||||
export default [
|
||||
{
|
||||
area: 'top',
|
||||
container: 'FooPage',
|
||||
injectedComponent: BarContainer,
|
||||
plugin: 'my-plugin',
|
||||
},
|
||||
];
|
||||
```
|
||||
Just by doing so, the `another-plugin` will add a `Button` which toggles the `lorem` paragraph in the `FooPage` view.
|
||||
|
||||
***
|
||||
|
||||
## Routeless container store injection
|
||||
|
||||
See the basic container's store injection [documentation](./development.md#using-redux-sagas).
|
||||
|
||||
If you have a container which can be a child of several other containers (i.e. it doesn't have a route); you'll have to inject it directly in the `./plugins/my-plugin/admin/src/containers/App/index.js` file as follows :
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/App/index.js`.
|
||||
```js
|
||||
// ...
|
||||
import fooReducer from 'containers/Foo/reducer';
|
||||
import fooSaga from 'container/Foo/sagas';
|
||||
|
||||
import saga from './sagas';
|
||||
import { makeSelectFoo } from './selectors';
|
||||
|
||||
// ...
|
||||
|
||||
export class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
<Switch>
|
||||
{*/ List of all your routes here */}
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
// ...
|
||||
});
|
||||
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
// Foo reducer
|
||||
const withFooReducer = injectReducer({ key: 'foo', reducer: fooReducer });
|
||||
// Global reducer
|
||||
const withReducer = injectReducer({ key: 'global', reducer });
|
||||
// Foo saga
|
||||
const withFooSaga = injectSaga({ key: 'foo', saga: fooSaga });
|
||||
// Global saga
|
||||
const withSaga = injectSaga({ key: 'global', saga });
|
||||
|
||||
export default compose(
|
||||
withFooReducer,
|
||||
withReducer,
|
||||
withFooSaga,
|
||||
withSaga,
|
||||
withConnect,
|
||||
)(App);
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Execute a logic before mounting
|
||||
|
||||
You can execute a business logic before your plugin is being mounted.
|
||||
|
||||
### Usage
|
||||
|
||||
To do this, you need to create `bootstrap.js` file at the root of your `src` plugin's folder.
|
||||
This file must contains a default functions that returns a `Promise`.
|
||||
|
||||
#### Example
|
||||
|
||||
In this example, we want to populate the left menu with links that will refer to our Content Types.
|
||||
|
||||
**Path —** `./app/plugins/content-manager/admin/src/bootstrap.js`.
|
||||
```js
|
||||
import { generateMenu } from 'containers/App/sagas';
|
||||
|
||||
// This method is executed before the load of the plugin
|
||||
const bootstrap = (plugin) => new Promise((resolve, reject) => {
|
||||
generateMenu()
|
||||
.then(menu => {
|
||||
plugin.leftMenuSections = menu;
|
||||
|
||||
resolve(plugin);
|
||||
})
|
||||
.catch(e => reject(e));
|
||||
});
|
||||
|
||||
export default bootstrap;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Prevent rendering
|
||||
|
||||
You can prevent your plugin from being rendered if some conditions aren't met.
|
||||
|
||||
### Usage
|
||||
|
||||
To disable your plugin's rendering, you can simply create `requirements.js` file at the root of your `src` plugin's folder.
|
||||
This file must contain a default function that returns a `Promise`.
|
||||
|
||||
#### Example
|
||||
|
||||
Let's say that you want to disable your plugin if the server autoReload config is disabled in development mode.
|
||||
|
||||
**Path —** `./app/config/environments/development/server.json`.
|
||||
```
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 1337,
|
||||
"autoReload": {
|
||||
"enabled": true
|
||||
},
|
||||
"cron": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You'll first create a request to check if the `autoReload` config is enabled.
|
||||
|
||||
**Path —** `./app/plugins/my-plugin/config/routes.json`.
|
||||
```json
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/autoReload",
|
||||
"handler": "MyPlugin.autoReload",
|
||||
"config": {
|
||||
"policies": []
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
Then the associated handler:
|
||||
|
||||
**Path —** `./app/plugins/my-plugin/controllers/MyPlugin.js`.
|
||||
```js
|
||||
const _ = require('lodash');
|
||||
const send = require('koa-send');
|
||||
|
||||
module.exports = {
|
||||
autoReload: async ctx => {
|
||||
ctx.send({ autoReload: _.get(strapi.config.environments, 'development.server.autoReload', false) });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Finally, you'll create a file called `requirements.js`at the root of your plugin's src folder.
|
||||
|
||||
The default function exported must return a `Promise`.
|
||||
If you wan't to prevent the plugin from being rendered you'll have to set `plugin.preventComponentRendering = true;`.
|
||||
In this case, you'll have to set:
|
||||
```js
|
||||
plugin.blockerComponentProps = {
|
||||
blockerComponentTitle: 'my-plugin.blocker.title',
|
||||
blockerComponentDescription: 'my-plugin.blocker.description',
|
||||
blockerComponentIcon: 'fa-refresh',
|
||||
};
|
||||
```
|
||||
|
||||
To follow the example above:
|
||||
|
||||
**Path —** `./app/plugins/my-plugin/admin/src/requirements.js`.
|
||||
```js
|
||||
// Use our request helper
|
||||
import request from 'utils/request';
|
||||
|
||||
const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
|
||||
request('/my-plugin/autoReload')
|
||||
.then(response => {
|
||||
// If autoReload is enabled the response is `{ autoReload: true }`
|
||||
plugin.preventComponentRendering = !response.autoReload;
|
||||
// Set the BlockerComponent props
|
||||
plugin.blockerComponentProps = {
|
||||
blockerComponentTitle: 'my-plugin.blocker.title',
|
||||
blockerComponentDescription: 'my-plugin.blocker.description',
|
||||
blockerComponentIcon: 'fa-refresh',
|
||||
blockerComponentContent: 'renderIde', // renderIde will add an ide section that shows the development environment server.json config
|
||||
};
|
||||
|
||||
return resolve(plugin);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
|
||||
export default shouldRenderCompo;
|
||||
```
|
||||
|
||||
### Customization
|
||||
|
||||
You can render your own custom blocker by doing as follows:
|
||||
|
||||
**Path —** `./app/plugins/my-plugin/admin/src/requirements.js`.
|
||||
```js
|
||||
// Use our request helper
|
||||
import request from 'utils/request';
|
||||
|
||||
// Your custom blockerComponentProps
|
||||
import MyCustomBlockerComponent from 'components/MyCustomBlockerComponent';
|
||||
|
||||
const shouldRenderCompo = (plugin) => new Promise((resolve, request) => {
|
||||
request('/my-plugin/autoReload')
|
||||
.then(response => {
|
||||
// If autoReload is enabled the response is `{ autoReload: true }`
|
||||
plugin.preventComponentRendering = !response.autoReload;
|
||||
|
||||
// Tell which component to be rendered instead
|
||||
plugin.blockerComponent = MyCustomBlockerComponent;
|
||||
|
||||
return resolve(plugin);
|
||||
})
|
||||
.catch(err => reject(err));
|
||||
});
|
||||
|
||||
export default shouldRenderCompo;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Using React/Redux and sagas
|
||||
|
||||
If your application is going to interact with some back-end application for data, we recommend using redux saga for side effect management.
|
||||
This short tutorial will show how to fetch data using actions/reducer/sagas.
|
||||
|
||||
### Constants declaration
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/constants.js`
|
||||
```js
|
||||
export const DATA_FETCH = 'MyPlugin/FooPage/DATA_FETCH';
|
||||
export const DATA_FETCH_ERROR = 'MyPlugin/FooPage/DATA_FETCH_ERROR';
|
||||
export const DATA_FETCH_SUCCEEDED = 'MyPlugin/FooPage/DATA_FETCH_SUCCEEDED';
|
||||
```
|
||||
|
||||
### Actions declaration
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/actions.js`
|
||||
```js
|
||||
import {
|
||||
DATA_FETCH,
|
||||
DATA_FETCH_ERROR,
|
||||
DATA_FETCH_SUCCEEDED,
|
||||
} from './constants';
|
||||
|
||||
export function dataFetch(params) {
|
||||
return {
|
||||
type: DATA_FETCH,
|
||||
params,
|
||||
};
|
||||
}
|
||||
|
||||
export function dataFetchError(errorMessage) {
|
||||
return {
|
||||
type: DATA_FETCH_ERROR,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export function dataFetchSucceeded(data) {
|
||||
return {
|
||||
type: DATA_FETCH_SUCCEEDED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Reducer
|
||||
|
||||
We strongly recommend to use [Immutable.js](https://facebook.github.io/immutable-js/) to structure your data.
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/reducer.js`
|
||||
```js
|
||||
import { fromJS, Map } from 'immutable';
|
||||
import {
|
||||
DATA_FETCH_ERROR,
|
||||
DATA_FETCH_SUCCEEDED,
|
||||
} from './constants';
|
||||
|
||||
const initialState = fromJS({
|
||||
data: Map({}),
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
loading: true,
|
||||
});
|
||||
|
||||
function fooPageReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DATA_FETCH_ERROR:
|
||||
return state
|
||||
.set('error', true)
|
||||
.set('errorMessage', action.errorMessage)
|
||||
.set('loading', false);
|
||||
case DATA_FETCH_SUCCEEDED:
|
||||
return state
|
||||
.set('data', Map(action.data))
|
||||
.set('error', false)
|
||||
.set('errorMessage', '')
|
||||
.set('loading', false);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default fooPageReducer;
|
||||
```
|
||||
|
||||
### Sagas
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/sagas.js`
|
||||
```js
|
||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||
import { takeLatest, put, fork, call, take, cancel } from 'redux-saga/effects';
|
||||
|
||||
// Use our request helper
|
||||
import { request } from 'utils/request';
|
||||
|
||||
// Actions
|
||||
import { dataFetchError, dataFetchSucceeded } from './actions';
|
||||
import { DATA_FETCH } from './constants';
|
||||
|
||||
export function* fetchData(action) {
|
||||
try {
|
||||
const requestUrl = `/baseUrl/${action.params}`;
|
||||
const opts = {
|
||||
method: 'GET',
|
||||
};
|
||||
|
||||
// Fetch data
|
||||
const response = yield call(request, requestUrl, opts);
|
||||
|
||||
// Pass the response to the reducer
|
||||
yield put(dataFetchSucceeded(response));
|
||||
|
||||
} catch(error) {
|
||||
yield put(dataFetchError(error));
|
||||
}
|
||||
}
|
||||
|
||||
// Individual export for testing
|
||||
function* defaultSaga() {
|
||||
// Listen to DATA_FETCH event
|
||||
const fetchDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);
|
||||
|
||||
// Cancel watcher
|
||||
yield take(LOCATION_CHANGE);
|
||||
|
||||
yield cancel(fetchDataWatcher);
|
||||
}
|
||||
|
||||
export default defaultSaga;
|
||||
```
|
||||
|
||||
N.B. You can use a selector in your sagas :
|
||||
|
||||
```js
|
||||
import { put, select, fork, call, take, cancel } from 'redux-saga/effects';
|
||||
import { makeSelectUserName } from './selectors';
|
||||
|
||||
export function* foo() {
|
||||
try {
|
||||
const userName = yield select(makeSelectUserName());
|
||||
|
||||
// ...
|
||||
} catch(error) {
|
||||
// ...
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSaga() {
|
||||
// ...
|
||||
}
|
||||
|
||||
export default defaultSaga;
|
||||
```
|
||||
|
||||
|
||||
### Selectors
|
||||
|
||||
[Reselect](https://github.com/reactjs/reselect) is a library used for slicing your redux state and providing only the relevant sub-tree to a react component. It has three key features:
|
||||
|
||||
1. Computational power
|
||||
2. Memoization
|
||||
3. Composability
|
||||
|
||||
Creating a selector:
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/selectors.js`
|
||||
```js
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
/**
|
||||
* Direct selector to the fooPage state domain
|
||||
*/
|
||||
const selectFooPageDomain = () => state => state.get('fooPage');
|
||||
|
||||
/**
|
||||
* Other specific selectors
|
||||
*/
|
||||
|
||||
const makeSelectLoading = () => createSelector(
|
||||
selectFooPageDomain(),
|
||||
(substate) => substate.get('loading'),
|
||||
);
|
||||
|
||||
/**
|
||||
* Default selector used by FooPage
|
||||
*/
|
||||
|
||||
const selectFooPage = () => createSelector(
|
||||
selectFooDomain(),
|
||||
(substate) => substate.toJS()
|
||||
);
|
||||
|
||||
export default selectFooPage;
|
||||
export { makeSelectLoading };
|
||||
|
||||
```
|
||||
|
||||
|
||||
#### Example
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`
|
||||
```js
|
||||
import React from 'react';
|
||||
import { bindActionCreators } from 'redux';
|
||||
import { connect, compose } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Main router
|
||||
import { router } from 'app';
|
||||
|
||||
// Utils
|
||||
import injectSaga from 'utils/injectSaga';
|
||||
import injectReducer from 'utils/injectReducer';
|
||||
|
||||
// Actions
|
||||
import { dataFetch } from './actions';
|
||||
// sagas
|
||||
import saga from './sagas';
|
||||
// Selectors
|
||||
import selectFooPage from './selectors';
|
||||
// Reducer
|
||||
import reducer from './reducer';
|
||||
|
||||
export class FooPage extends React.Component {
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (this.props.error !== nextProps.error && nextProps.error) {
|
||||
strapi.notification.error(nextProps.errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps) {
|
||||
if (prevProps.match.pathname !== this.props.pathname) {
|
||||
this.props.dataFetch(this.props.match.params.bar);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.props.error) return <div>An error occurred</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h4>Data display</h4>
|
||||
<span>{this.props.data.foo}</span>
|
||||
<span>{this.props.data.bar}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
FooPage.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
dataFetch: PropTypes.func.isRequired,
|
||||
error: PropTypes.bool.isRequired,
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
match: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = selectFoo();
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
dataFetch,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
const withReducer = injectReducer({ key: 'fooPage', reducer });
|
||||
const withSagas = injectSaga({ key: 'fooPage', saga });
|
||||
|
||||
export default compose(
|
||||
withReducer,
|
||||
withSagas,
|
||||
withConnect,
|
||||
)(FooPage);
|
||||
}
|
||||
```
|
||||
@ -1,506 +0,0 @@
|
||||
# Development
|
||||
|
||||
Any Strapi plugin can contain two parts: an [API](#plugin-api-development) and a [plugin admin interface](#plugin-admin-interface-development). This section explains how to change each of these two parts after plugin creation, or modify an existing plugin.
|
||||
|
||||
<!-- See the [strapi-generate](../cli/CLI.md#strapi-generateplugin) part to check the dedicated Strapi's command line. -->
|
||||
|
||||
***
|
||||
|
||||
## Back-end
|
||||
|
||||
This section explains how the 'backend part' of your plugin works.
|
||||
|
||||
Table of contents:
|
||||
- [Folders and files structure](#folders-and-files-structure)
|
||||
- [Routes](#routes)
|
||||
- [CLI](#cli)
|
||||
- [Controllers](#controllers)
|
||||
- [Models](#models)
|
||||
- [Policies](#policies)
|
||||
- [ORM queries](#orm-queries)
|
||||
|
||||
### Folders and files structure
|
||||
|
||||
The logic of a plugin is located at his root directory `./plugins/**`. The folders and files structure is the following:
|
||||
```
|
||||
/plugin
|
||||
└─── admin // Contains the plugin's front-end
|
||||
└─── config // Contains the configurations of the plugin
|
||||
| └─── routes.json // Contains the plugin's API routes
|
||||
└─── controllers // Contains the plugin's API controllers
|
||||
└─── models // Contains the plugin's API models
|
||||
└─── services // Contains the plugin's API services
|
||||
```
|
||||
|
||||
### Routes
|
||||
|
||||
The plugin API routes are defined in the `./plugins/**/config/routes.json` file.
|
||||
|
||||
> Please refer to [router documentation](../guides/routing.md) for informations.
|
||||
|
||||
**Route prefix**
|
||||
|
||||
Each route of a plugin is prefixed by the name of the plugin (eg: `/my-plugin/my-plugin-route`).
|
||||
|
||||
To disable the prefix, add the `prefix` attribute to each concerned route, like below:
|
||||
```json
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/my-plugin-route",
|
||||
"handler": "MyPlugin.action",
|
||||
"prefix": false
|
||||
}
|
||||
```
|
||||
|
||||
### CLI
|
||||
|
||||
The CLI can be used to generate files in the plugins folders.
|
||||
|
||||
Please refer to the [CLI documentation](../cli/CLI.md) for more informations.
|
||||
|
||||
### Controllers
|
||||
|
||||
Controllers contain functions executed according to the requested route.
|
||||
|
||||
Please refer to the [Controllers documentation](../guides/controllers.md) for more informations.
|
||||
|
||||
### Models
|
||||
|
||||
A plugin can have its own models.
|
||||
|
||||
##### Table/Collection naming
|
||||
|
||||
Sometimes it happens that the plugins inject models that have the same name as yours. Let's take a quick example.
|
||||
|
||||
You already have `User` model defining in your `./api/user/models/User.settings.json` API. And you decide to install the `Users & Permissions` plugin. This plugin also contains a `User` model. To avoid the conflicts, the plugins' models are not globally exposed which means you cannot access to the plugin's model like this:
|
||||
|
||||
```js
|
||||
module.exports = {
|
||||
findUser: async function (params) {
|
||||
// This `User` global variable will always make a reference the User model defining in your `./api/xxx/models/User.settings.json`.
|
||||
return await User.find();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Also, the table/collection name won't be `users` because you already have a `User` model. That's why, the framework will automatically prefix the table/collection name for this model with the name of the plugin. Which means in our example, the table/collection name of the `User` model of our plugin `Users & Permissions` will be `users-permissions_users`. If you want to force the table/collection name of the plugin's model, you can add the `collectionName` attribute in your model.
|
||||
|
||||
|
||||
Please refer to the [Models documentation](../guides/models.md) for more informations.
|
||||
|
||||
### Policies
|
||||
|
||||
#### Global policies
|
||||
|
||||
A plugin can also use a globally exposed policy in the current Strapi project.
|
||||
|
||||
```json
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"handler": "MyPlugin.index",
|
||||
"config": {
|
||||
"policies": [
|
||||
"global.isAuthenticated"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Plugin policies
|
||||
|
||||
A plugin can have its own policies, such as adding security rules. For instance, if the plugin includes a policy named `isAuthenticated`, the syntax to use this policy would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"routes": [
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/",
|
||||
"handler": "MyPlugin.index",
|
||||
"config": {
|
||||
"policies": [
|
||||
"plugins.myPlugin.isAuthenticated"
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Please refer to the [Policies documentation](../guides/policies.md) for more informations.
|
||||
|
||||
### ORM queries
|
||||
|
||||
Strapi supports multiple ORMs in order to let the users choose the database management system that suits their needs. Hence, each plugin must be compatible with at least one ORM. Each plugin contains a folder named `queries` in `./plugins/**/api/queries`. A folder must be created for each ORM (eg. `mongoose`) with a file named `mongoose.js` which exports the Mongoose ORM related queries.
|
||||
|
||||
The queries are accessible through the `strapi.query()` method, which automatically contains the queries according to the ORM used by the model.
|
||||
|
||||
#### Example
|
||||
|
||||
Mongoose ORM queries definition:
|
||||
|
||||
**Path —** `./plugins/my-plugin/api/config/queries/mongoose/index.js`.
|
||||
```js
|
||||
module.exports = {
|
||||
getUsers: async (params) => {
|
||||
return User.find(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Bookshelf ORM queries definition:
|
||||
|
||||
**Path —** `./plugins/my-plugin/api/config/queries/bookshelf/index.js`.
|
||||
```js
|
||||
module.exports = {
|
||||
getUsers: async (params) => {
|
||||
return User.fetchAll(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage from the plugin:
|
||||
|
||||
**Path —** `./plugins/my-plugin/api/controllers/index.js`.
|
||||
```js
|
||||
module.exports = {
|
||||
getUsers: async () => {
|
||||
// Get parameters from the request
|
||||
const { limit, sort } = ctx.request.query;
|
||||
|
||||
// Get the list of users using the plugins queries
|
||||
const users = await strapi.query('User').getUsers({ limit, sort });
|
||||
|
||||
// Send the list of users as response
|
||||
ctx.body = users;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Advanced usage
|
||||
|
||||
Each function in the query file is bound with the ORM's model. It means that you can create generic query very easily. This feature is useful for CRUD such as we did in the [Content Manager plugin](https://github.com/strapi/strapi/tree/master/packages/strapi-plugin-content-manager/config/queries).
|
||||
|
||||
Mongoose ORM generic queries:
|
||||
|
||||
**Path —** `./plugins/my-plugin/api/config/queries/mongoose/index.js`.
|
||||
```js
|
||||
module.exports = {
|
||||
getAll: async function (params) {
|
||||
// this refers to the Mongoose model called in the query
|
||||
// ex: strapi.query('User').getAll(), this will be equal to the User Mongoose model.
|
||||
return this.find(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Bookshelf ORM generic queries:
|
||||
|
||||
**Path —** `./plugins/my-plugin/api/config/queries/bookshelf/index.js`.
|
||||
```js
|
||||
module.exports = {
|
||||
getAll: async function (params) {
|
||||
// this refers to the Bookshelf model called in the query
|
||||
// ex: strapi.query('User').getAll(), this will be equal to the User Bookshelf model.
|
||||
return this.fetchAll(params);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Usage from the plugin:
|
||||
|
||||
**Path —** `./plugins/my-plugin/api/controllers/index.js`.
|
||||
```js
|
||||
module.exports = {
|
||||
getUsers: async () => {
|
||||
// Get parameters from the request
|
||||
const { limit, sort } = ctx.request.query;
|
||||
|
||||
// Get the list of users using the plugin's queries
|
||||
const users = await strapi.query('User').getAll({ limit, sort });
|
||||
|
||||
// Send the list of users as response
|
||||
ctx.body = users;
|
||||
}
|
||||
}
|
||||
```
|
||||
***
|
||||
|
||||
## Front-end
|
||||
|
||||
This section explains how to create your plugin interface in the admin panel.
|
||||
|
||||
Table of contents:
|
||||
- [Development mode](#start-the-project-in-development-mode)
|
||||
- [Folders and files structure](#folders-and-file-structure)
|
||||
- [Routing](#routing)
|
||||
- [Using Redux/sagas](#using-redux-sagas)
|
||||
- [i18n](#i18n)
|
||||
- [Styling](#styles)
|
||||
- [Data flow](#data-flow)
|
||||
- [API Reference](#api-reference)
|
||||
- [Tutorial](#tutorial)
|
||||
|
||||
### Introduction
|
||||
|
||||
Strapi's admin panel and plugins system aim to be an easy and powerful way to create new features.
|
||||
|
||||
The admin panel is a [React](https://facebook.github.io/react/) application which can embed other React applications. These other React applications are the `admin` parts of each Strapi's plugins.
|
||||
|
||||
### Start the project in development mode
|
||||
|
||||
To start the project in development mode, read the [Contributing Guide](https://github.com/strapi/strapi/blob/master/.github/CONTRIBUTING.md).
|
||||
|
||||
### Folders and files structure
|
||||
|
||||
The admin panel related parts of each plugin is contained in the `./plugins/my-plugin/admin` folder it has the following structure:
|
||||
|
||||
```
|
||||
/admin
|
||||
└─── build // Webpack build of the plugin
|
||||
└─── src // Source code directory
|
||||
| └─── bootstrap.js // (Optional) Contains the logic to execute before rendering the plugin
|
||||
| └─── components // Contains the list of React components used by the plugin
|
||||
| └─── containers
|
||||
| | └─── App // Container used by every others containers
|
||||
| | └─── HomePage
|
||||
| | └─── action.js // List of Redux actions used by the current container
|
||||
| | └─── constants.js // List of actions constants
|
||||
| | └─── index.js // React component of the current container
|
||||
| | └─── reducer.js // Redux reducer used by the current container
|
||||
| | └─── sagas.js // List of sagas functions
|
||||
| | └─── selectors.js // List of selectors
|
||||
| | └─── styles.scss // Style of the current container
|
||||
| |
|
||||
| └─── requirements.js // (Optional) Contains the logic to prevent a plugin from being rendered
|
||||
| └─── translations // Contains the translations to make the plugin internationalized
|
||||
| └─── en.json
|
||||
| └─── fr.json
|
||||
└─── package.json // List of the necessary npm dependencies
|
||||
```
|
||||
|
||||
### Routing
|
||||
|
||||
The routing is based on the [React Router V4](https://reacttraining.com/react-router/web/guides/philosophy), due to it's implementation each route is declared in the `containers/App/index.js` file.
|
||||
|
||||
Also, we chose to use the [Switch Router](https://reacttraining.com/react-router/web/api/Switch) because it renders a route exclusively.
|
||||
|
||||
**Route declaration :**
|
||||
|
||||
Let's say that you want to create a route `/user` with params `/:id` associated with the container UserPage.
|
||||
|
||||
The declaration would be as followed :
|
||||
|
||||
**Path —** `plugins/my-plugin/admin/src/containers/App/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
import UserPage from 'containers/UserPage';
|
||||
|
||||
// ...
|
||||
|
||||
class App extends React.Component {
|
||||
// ...
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.myPlugin}>
|
||||
<Switch>
|
||||
<Route exact path="/plugins/my-plugin/user/:id" component={UserPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
```
|
||||
See the [advanced user navigation guide](./advanced.md#handle-user-navigation) for more informations.
|
||||
|
||||
### Using Redux sagas
|
||||
|
||||
Due to React Router V4 your container's store is not directly injected.
|
||||
To inject your container's store if it's associated with a route you have to do it manually.
|
||||
|
||||
As an example, you created a FooPage container associated with the route `/plugins/my-plugin/bar`, and you want to use redux/action/reducer/sagas.
|
||||
|
||||
Your `plugins/my-plugin/admin/src/containers/App/index.js` file will look as follows :
|
||||
|
||||
```js
|
||||
render() => (
|
||||
<div className={styles.myPlugin}>
|
||||
<Switch>
|
||||
<Route exact path="/plugins/my-plugin/bar" component={FooPage} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
```
|
||||
|
||||
And the `plugins/my-plugin/admin/src/containers/FooPage/index.js` file will be :
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
// Utils to create your container store
|
||||
import injectReducer from 'utils/injectReducer';
|
||||
import injectSaga from 'utils/injectSaga';
|
||||
|
||||
import {
|
||||
foo,
|
||||
bar,
|
||||
} from './actions';
|
||||
import reducer from './reducer';
|
||||
import saga from './sagas';
|
||||
import { makeSelectFooPage } from './selectors';
|
||||
|
||||
// Styles
|
||||
import styles from './styles.scss';
|
||||
|
||||
export class FooPage extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.fooPage}>
|
||||
Awesome container
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FooPage.propTypes = {
|
||||
fooPage: PropTypes.any,
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
foo,
|
||||
bar,
|
||||
},
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = createStructuredSelector({
|
||||
fooPage: makeSelectFooPage(),
|
||||
});
|
||||
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
// This is where you create your container's store
|
||||
// the key must correspond to your container name in camelCase
|
||||
const withSagas = injectSaga({ key: 'fooPage', saga });
|
||||
const withReducer = injectReducer({ key: 'fooPage', reducer });
|
||||
|
||||
export default compose(
|
||||
withReducer,
|
||||
withSagas,
|
||||
withConnect,
|
||||
)(FooPage);
|
||||
```
|
||||
|
||||
|
||||
Important: see the [advanced container store injection](./advanced.md#routeless-container-store-injection.md) for more informations about how to create your container's store.
|
||||
|
||||
### i18n
|
||||
|
||||
[React Intl](https://github.com/yahoo/react-intl) provides React components and an API to format dates, numbers, and strings, including pluralization and handling translations.
|
||||
|
||||
**Usage**
|
||||
|
||||
We recommend to set all your components text inside the translations folder.
|
||||
|
||||
The example below shows how to use i18n inside your plugin.
|
||||
|
||||
**Define all your ids with the associated message:**
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/en.json`.
|
||||
```json
|
||||
{
|
||||
"notification.error.message": "An error occurred"
|
||||
}
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/fr.json`
|
||||
```json
|
||||
{
|
||||
"notification.error.message": "Une erreur est survenue"
|
||||
}
|
||||
```
|
||||
|
||||
**Usage inside a component**
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/components/Foo/index.js`.
|
||||
```js
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import SomeOtherComponent from 'components/SomeOtherComponent';
|
||||
|
||||
const Foo = (props) => (
|
||||
<div className={styles.foo}>
|
||||
<FormattedMessage id="my-plugin.notification.error.message" />
|
||||
<SomeOtherComponent {...props} />
|
||||
</div>
|
||||
)
|
||||
|
||||
export default Foo;
|
||||
```
|
||||
|
||||
See [the documentation](https://github.com/yahoo/react-intl/wiki/Components#formattedmessage) for more extensive usage.
|
||||
|
||||
### Styles
|
||||
|
||||
The [Bootstrap styles](http://getbootstrap.com/) are inherited by the plugins. However, each component has its own styles, so it possible to completely customize it.
|
||||
|
||||
**See the [plugin styles](../concepts/concepts.md#plugin-styles) for informations on its concept.**
|
||||
|
||||
To style a plugin component:
|
||||
- Add a `styles.scss` file in the component directory
|
||||
- Require it from the `index.js` file (`import styles from './styles.scss';`)
|
||||
- Add some styles in the `styles.scss` file
|
||||
|
||||
```
|
||||
.wrapper {
|
||||
display: block;
|
||||
background: red;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
}
|
||||
```
|
||||
|
||||
Use this style from the component: `<div className={styles.wrapper}></div>`.
|
||||
|
||||
Note: if you want to use several classes:
|
||||
|
||||
```js
|
||||
import cn from 'classnames';
|
||||
|
||||
// ...
|
||||
|
||||
return (
|
||||
<div className={cn(styles.wrapper, styles.otherClass)}>{this.props.children}</div>
|
||||
);
|
||||
|
||||
// ...
|
||||
|
||||
```
|
||||
|
||||
### Data flow
|
||||
|
||||
Each plugin has its own data store, so it stays completely independent from the others.
|
||||
|
||||
Data flow is controlled thanks to Redux and redux-sagas.
|
||||
|
||||
### API Reference
|
||||
|
||||
> Refer to the [plugin registration](../api-reference/reference.md#plugin-registration) for details.
|
||||
|
||||
### Tutorial
|
||||
|
||||
For more information, try the [Create your first Strapi plugin](http://strapi.io) tutorial.
|
||||
@ -1,130 +0,0 @@
|
||||
# Plugin Menu library
|
||||
|
||||
```js
|
||||
// ...
|
||||
|
||||
import PluginLeftMenu from 'components/PluginLeftMenu';
|
||||
|
||||
// ...
|
||||
|
||||
const Foo = (props) => {
|
||||
const sections = [
|
||||
{
|
||||
name: 'section 1',
|
||||
items: [
|
||||
{ icon: 'fa-caret-square-o-right', name: 'link 1'},
|
||||
{ icon: 'fa-caret-square-o-right', name: 'link 2'},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.foo}>
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<PluginLeftMenu
|
||||
sections={sections}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Foo;
|
||||
|
||||
// ...
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
| Property | Type | Required | Description
|
||||
:---| :---| :---| :---
|
||||
| `addCustomSection` | function | no | Allows to add another section after the initial one. |
|
||||
| `basePath` | string | yes | For example the basePath of the route '/plugins/my-plugin/foo/bar' is 'my-plugin/categories' |
|
||||
| `renderCustomLink` | function | no | Allows to override the design and the behavior of a link |
|
||||
| `sections` | array | yes | Sections of the component menu |
|
||||
|
||||
## Example
|
||||
|
||||
```js
|
||||
// ...
|
||||
|
||||
import PluginLeftMenu from 'components/PluginLeftMenu';
|
||||
|
||||
// ...
|
||||
|
||||
const addCustomSection = (sectionStyles) => (
|
||||
// You have access to the section styles
|
||||
<div className={sectionStyles.pluginLeftMenuSection}>
|
||||
<p>
|
||||
DOCUMENTATION
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
Read more about strapi in our <a href="http://strapi.io/documentation" target="_blank">documentation</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderAddLink = (props, customLinkStyles) => (
|
||||
<li className={customLinkStyles.pluginLeftMenuLink}>
|
||||
<div className={`${customLinkStyles.liInnerContainer}`} onClick={this.handleAddLinkClick}>
|
||||
<div>
|
||||
<i className={`fa ${props.link.icon}`} />
|
||||
</div>
|
||||
<span>{props.link.name}</span>
|
||||
</div>
|
||||
</li>
|
||||
)
|
||||
|
||||
const renderCustomLink = (props, linkStyles) => {
|
||||
if (props.link.name === 'bar') return this.renderAddLink(props, linkStyles);
|
||||
|
||||
return (
|
||||
<li className={linkStyles.pluginLeftMenuLink}>
|
||||
<NavLink className={linkStyles.link} to={`/plugins/my-plugin/foo/${props.link.name}`} activeClassName={linkStyles.linkActive}>
|
||||
<div>
|
||||
<i className={`fa fa-caret-square-o-right`} />
|
||||
</div>
|
||||
<div className={styles.contentContainer}>
|
||||
<span className={spanStyle}>{props.link.name}</span>
|
||||
</div>
|
||||
|
||||
</NavLink>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
const Foo = (props) => {
|
||||
const sections = [
|
||||
{
|
||||
name: 'section 1',
|
||||
items: [
|
||||
{ icon: 'fa-caret-square-o-right', name: 'link 1'},
|
||||
{ icon: 'fa-caret-square-o-right', name: 'link 2'},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.foo}>
|
||||
<div className="container-fluid">
|
||||
<div className="row">
|
||||
<PluginLeftMenu
|
||||
addCustomSection={addCustomSection}
|
||||
sections={sections}
|
||||
renderCustomLink={renderCustomLink}
|
||||
basePath="my-plugins/foo"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
export default Foo;
|
||||
```
|
||||
@ -1,31 +0,0 @@
|
||||
# Quick start
|
||||
|
||||
You can install and uninstall any plugin you want.
|
||||
|
||||
## Plugin installation
|
||||
|
||||
Look at the [CLI documentation](../cli/CLI.html#strapi-install) to install plugin.
|
||||
|
||||
### Basic usage
|
||||
|
||||
Considering you want to install a plugin named `content-manager` you can run the following command:
|
||||
```bash
|
||||
strapi install content-manager
|
||||
```
|
||||
|
||||
> Note: This implies that this plugin is published on the npm registry as `strapi-plugin-content-manager`.
|
||||
|
||||
***
|
||||
|
||||
## Plugin uninstallation
|
||||
|
||||
Look at the [CLI documentation](../cli/CLI.html#strapi-uninstall) to install plugin.
|
||||
|
||||
### Basic usage
|
||||
|
||||
This command will simply removes the plugin folder.
|
||||
```bash
|
||||
strapi uninstall content-manager
|
||||
```
|
||||
|
||||
Please refer to the [CLI documentation](../cli/CLI.md) for more information.
|
||||
@ -1,429 +0,0 @@
|
||||
# UI components
|
||||
|
||||
Strapi provides built-in UI Components to make development faster.
|
||||
|
||||
## Button
|
||||
|
||||
Button library based on bootstrap classes.
|
||||
|
||||
{% center %}  {% endcenter %}
|
||||
|
||||
### Usage
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| -------- | ---- | -------- | ----------- |
|
||||
| `children`| node | no | Ex: `<Button primary>Click me</Button>` |
|
||||
| `className` | any | no | Sets a custom className. Ex: `<Button className={styles.myCustomClass} label="Click me" />` |
|
||||
| `kind` | string | no | Sets the built-in className to the button. Ex: `<Button kind="primaryAddShape" label="Click me" />` |
|
||||
| `label` | string | no | Sets the button label with i18n Ex: `<Button label="myPlugin.button.label" primary />` |
|
||||
| `labelValue` | string | no | Sets the button label with i18n and a dynamic value Ex: {% raw %} ```<Button label="myPlugin.button.label" labelValue={{ foo: 'bar' }} primary />``` {% endraw %} |
|
||||
| `loader` | bool | no | Displays a button loader. Ex: `<Button loader />` |
|
||||
| `primary` | bool | no | [Bootstrap className](https://v4-alpha.getbootstrap.com/components/buttons/) |
|
||||
| `primaryAddShape` | bool | no | Inserts fontAwesone plus icon inside the button. Ex: `<Button primaryAddShape>Click me</Button>` |
|
||||
| `secondary`| bool | no | [Bootstrap className](https://v4-alpha.getbootstrap.com/components/buttons/) |
|
||||
| `secondaryHotline` | bool | no | Sets className |
|
||||
| `secondaryHotlineAdd` | bool | no | Inserts fontAwesone plus icon inside the button. Ex: `<Button secondaryHotlineAdd>Click me</Button>` |
|
||||
| `type` | string | no | Sets the button type |
|
||||
|
||||
### Example
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/en.json`.
|
||||
```json
|
||||
{
|
||||
"myPlugin.button.label": "Add a new"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/components/Foo/index.js`.
|
||||
```js
|
||||
// Make sure you don't have any other component called Button otherwise it will
|
||||
// import the one from your ./components folder instead.
|
||||
import React from 'react';
|
||||
|
||||
import Button from 'components/Button';
|
||||
import styles from './styles.scss';
|
||||
|
||||
function Foo() {
|
||||
// Define your buttons
|
||||
const buttons = [
|
||||
{
|
||||
kind: 'primaryAddShape',
|
||||
label: 'myPlugin.button.label',
|
||||
labelValues: {
|
||||
foo: 'Bar',
|
||||
},
|
||||
onClick: () => console.log('Click'),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.foo}>
|
||||
{buttons.map(buttonProps => <Button key={buttonProps.label} {...buttonProps} />)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Same as
|
||||
// return (
|
||||
// <div className={styles.foo}>
|
||||
// <Button
|
||||
// label="myPlugin.button.label"
|
||||
// labelValues={{ foo: 'Bar' }}
|
||||
// onClick={() => console.log('Click')}
|
||||
// primaryAddShape
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
}
|
||||
|
||||
// Will display a primaryAddShape button with label: 'Add a new Bar'
|
||||
export default Foo;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## ExtendComponent
|
||||
|
||||
ExtendComponent allows a plugin to injectDesign into another one.
|
||||
|
||||
> Refer to the advanced plugin [documentation](./advanced.md#inject-design) to see how to use it.
|
||||
|
||||
***
|
||||
|
||||
## Ico
|
||||
|
||||
Ico components that works with fontAwesome.
|
||||
|
||||
### Usage
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| -------- | ---- | -------- | ----------- |
|
||||
| `icoType` | string | no (default: `trash`) | fontAwesome ico name. Ex: <Ico icoType="pencil" /> |
|
||||
| `onClick` | func | no | Function executed onClick. |
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import Ico from 'components/Ico';
|
||||
import PopUpWarning from 'components/PopUpWarning';
|
||||
import styles from 'styles';
|
||||
|
||||
class FooPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { showModal: false };
|
||||
}
|
||||
|
||||
handleClick = () => this.setState({ showModal: true });
|
||||
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.fooPage}>
|
||||
<Ico icoType="trash" onClick={this.handleClick} />
|
||||
<PopUpWarning
|
||||
isOpen={this.state.showModal}
|
||||
onConfirm={() => this.setState({ showModal: false })}
|
||||
toggleModal={() => this.setState({ showModal: false })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FooPage;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## IcoContainer
|
||||
|
||||
Container containing icons, generally used for editing or deleting data.
|
||||
|
||||
### Usage
|
||||
| Property | Type | Required | Description |
|
||||
| -------- | ---- | -------- | ----------- |
|
||||
| icons | array | no | Array containing icons' props. |
|
||||
|
||||
### Example
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import IcoContainer from 'components/IcoContainer';
|
||||
import PopUpWarning from 'components/PopUpWarning';
|
||||
import styles from 'styles';
|
||||
|
||||
class FooPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { showModal: false };
|
||||
}
|
||||
|
||||
handleClick = () => this.setState({ showModal: true });
|
||||
|
||||
render() {
|
||||
const icons = [
|
||||
{ icoType: 'pencil', onClick: () => console.log('click on pencil icon') },
|
||||
{ icoType: 'trash', onClick: this.handleClick },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={styles.fooPage}>
|
||||
<IcoContainer icons={icons} />
|
||||
<PopUpWarning
|
||||
isOpen={this.state.showModal}
|
||||
onConfirm={() => this.setState({ showModal: false })}
|
||||
toggleModal={() => this.setState({ showModal: false })}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default FooPage;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## Input
|
||||
|
||||
Strapi provides a built-in input library which includes :
|
||||
- All kind of inputs
|
||||
- Front-End validations
|
||||
- Error highlight
|
||||
- i18n
|
||||
|
||||
### Usage
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| -------- | ---- | -------- | ----------- |
|
||||
| `addon` | string | no | Allows to add a string addon in your input, based on [Bootstrap](https://v4-alpha.getbootstrap.com/components/input-group/#basic-example). Ex: `<Input {...this.props} addon="@" />` |
|
||||
| `addRequiredInputDesign` | bool | no | Allows to add an asterix on the input. Ex: `<Input {...this.props} addRequiredInputDesign />` |
|
||||
| `customBootstrapClass` | string | no | Allows to override the input bootstrap col system. Ex: `<Input {...this.props} customBootstrapClass="col-md-6 offset-md-6 pull-md-6" />` |
|
||||
| `deactivateErrorHighlight` | bool | no | Prevents from displaying error highlight in the input: Ex: `<Input {...this.props} deactivateErrorHighlight />` |
|
||||
| `didCheckErrors` | bool | no | Use this props to display errors after submitting a form. Ex: `<Input {...this.props} didCheckErrors={this.state.error} />` |
|
||||
| `disabled` | bool | no | Disable the input. Ex: `<Input {...this.props} disabled />` |
|
||||
| `errors` | array | no | Allows to display custom error messages. Ex: `<Input {...this.props} errors={[{ id: 'components.Input.error.custom-error', errorMessage: 'Something is wrong' }]} />` |
|
||||
| `inputDescription` | string | no | Allows to add an input description that is displayed like [bootstrap](https://v4-alpha.getbootstrap.com/components/forms/#defining-states). |
|
||||
| `label` | string | yes | Displays the input's label with i18n. |
|
||||
| `linkContent` | object | no | Allows to display a link within the input's description. Ex: {% raw %} ``` <Input {...this.props} linkContent={{ description: 'check out our', link: 'tutorial video' }} />``` {% endraw %} |
|
||||
| `name` | string | yes | The key to update your reducer. |
|
||||
| `noErrorsDescription` | bool | no | Prevents from displaying built-in errors. |
|
||||
| `onBlur` | func or bool | no | Overrides the default onBlur behavior. If bool passed to the component it will disabled the input validations checking. |
|
||||
| `onChange` | func | yes | Sets your reducer state. |
|
||||
| `onFocus` | func | no | Adds an onFocus event to the input. |
|
||||
| `placeholder` | string | no | Allows to set a placeholder. |
|
||||
| `selectOptions` | array | no | Options for the select. |
|
||||
| `tabIndex` | string | no | Sets the order in which the inputs are focused on tab key press. |
|
||||
| `title` | string | no | This props can only be used for checkboxes, it allows to add a title on top of the input, the label will be on the right side of the checkbox. |
|
||||
| `validations` | object | yes | Allows to have the built-in input's validations. If set to {} the validations will be ignored. Ex: {% raw %} ``` <Input {...this.props} validations={{ required: true }} />``` {% endraw %} |
|
||||
| `value` | string or bool or number | yes | The input's value. |
|
||||
|
||||
### Example
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
// Make sure you don't have a component called Input inside your ./components folder
|
||||
// It will import the one in your components folder instead.
|
||||
import Input from 'components/Input';
|
||||
|
||||
class FooPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state {
|
||||
data: {
|
||||
foo: 'bar',
|
||||
},
|
||||
error: false,
|
||||
errors: [],
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = ({ target }) => {
|
||||
const value = target.type === 'number' ? Number(target.value) : target.value;
|
||||
const error = target.value.length === 0;
|
||||
const data = {
|
||||
[target.name]: value,
|
||||
}
|
||||
if (error) {
|
||||
this.setState({ error: true, errors: [{ id: 'This input is required ' }] });
|
||||
} else {
|
||||
this.setState({ data, error: false, errors: [] });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.fooPage}>
|
||||
<Input
|
||||
type="string"
|
||||
value={this.state.data.foo}
|
||||
label="This is a string input"
|
||||
name="foo"
|
||||
onChange={this.handleChange}
|
||||
validations={{ required: true }}
|
||||
didCheckErrors={this.state.error}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
export default FooPage;
|
||||
```
|
||||
|
||||
#### Example with property linkContent and i18n
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/en.json`.
|
||||
```json
|
||||
{
|
||||
"form.input.inputDescription": "Content type name should be singular",
|
||||
"form.input.label": "Name",
|
||||
"form.input.linkContent.description": "check out our documentation"
|
||||
}
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/fr.json`.
|
||||
```json
|
||||
{
|
||||
"form.input.inputDescription": "Le nom des modèles doit être au singulier",
|
||||
"form.input.label": "Nom",
|
||||
"form.input.linkContent.description": "regardez la documentation."
|
||||
}
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
|
||||
// ...
|
||||
|
||||
class FooPage extends React.Component {
|
||||
// ...
|
||||
render () {
|
||||
return (
|
||||
<div className={styles.fooPage}>
|
||||
<Input
|
||||
didCheckErrors={this.state.error}
|
||||
inputDescription="my-plugin.form.input.inputDescription"
|
||||
label="my-plugin.form.input.label"
|
||||
linkContent={{ link: 'https://strapi.io/documentation/', description: 'my-plugin.form.input.linkContent.description' }}
|
||||
onChange={this.handleChange}
|
||||
type="string"
|
||||
validations={{ required: true }}
|
||||
value={this.state.data.foo}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ...
|
||||
|
||||
export default FooPage;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## PopUp Warning
|
||||
|
||||
PopUp warning library based on [reactstrap](https://reactstrap.github.io/components/modals/).
|
||||
|
||||
{% center %}  {% endcenter %}
|
||||
|
||||
### Usage
|
||||
|
||||
| Property | Type | Required | Description |
|
||||
| -------- | ---- | -------- | ----------- |
|
||||
| content | object | no | Used to set the confirm button, cancel button, title, body messages. |
|
||||
| onConfirm | func | yes | Function executed when the user clicks on the `Confirm button`. |
|
||||
| isOpen | bool | yes | Show or hide the popup. |
|
||||
| onlyConfirmButton | bool | yes | Display only the confirm button (`primary`) with `width: 100%`. |
|
||||
| popUpWarningType | string | yes | Sets the popup body icon. Available types: `danger`, `info`, `notFound`, `success`, `warning` |
|
||||
| toggleModal | func | yes | Function to toggle the modal. |
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/en.json`.
|
||||
```json
|
||||
{
|
||||
"popup.danger.button.cancel": "Cancel...",
|
||||
"popup.danger.button.confirm": "Confirm../",
|
||||
"popup.danger.message": "Are you sure you want to delete this item?!",
|
||||
"popup.danger.title": "Please confirm...."
|
||||
}
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/translations/fr.json`.
|
||||
```json
|
||||
{
|
||||
"popup.danger.button.cancel": "Annuler...",
|
||||
"popup.danger.button.label": "Je confirme...",
|
||||
"popup.danger.message": "Êtes-vous certain de vouloir supprimer ce message?!",
|
||||
"popup.danger.title": "Merci de confirmer..."
|
||||
}
|
||||
```
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/FooPage/index.js`.
|
||||
```js
|
||||
// ...
|
||||
|
||||
import Button from 'components/Button';
|
||||
import PopUpWarning from 'components/PopUpWarning';
|
||||
|
||||
// ...
|
||||
|
||||
class FooPage extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
isOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
handlePopUpConfirm = () => {
|
||||
// Some logic Here
|
||||
this.setState({ isOpen: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
const popupContent = {
|
||||
cancel: 'my-plugin.popup.danger.button.cancel',
|
||||
confirm: 'my-plugin.popup.danger.button.confirm',
|
||||
message: 'my-plugin.popup.danger.message',
|
||||
title: 'my-plugin.popup.danger.title',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Button primary onClick={() => this.setState({ isOpen: !this.state.isOpen })} label="my-plugin.button.label" />
|
||||
<PopUpWarning
|
||||
content={popupContent}
|
||||
onConfirm={this.handlePopUpConfirm}
|
||||
toggleModal={() => this.setState({ isOpen: !this.state.isOpen })}
|
||||
popUpWarningType="danger"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Equivalent without custom messages
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <Button primary onClick={() => this.setState({ isOpen: !this.state.isOpen })} label="my-plugin.button.label" />
|
||||
// <PopUpWarning
|
||||
// onConfirm={this.handlePopUpConfirm}
|
||||
// toggleModal={() => this.setState({ isOpen: !this.state.isOpen })}
|
||||
// popUpWarningType="danger"
|
||||
// />
|
||||
// </div>
|
||||
// );
|
||||
}
|
||||
}
|
||||
|
||||
export default FooPage;
|
||||
```
|
||||
@ -1,415 +0,0 @@
|
||||
# Helpers
|
||||
|
||||
Strapi provides helpers so you don't have to develop again and again the same generic functions.
|
||||
|
||||
## Request helper
|
||||
|
||||
A request helper is available to handle all requests inside a plugin.
|
||||
|
||||
It takes three arguments:
|
||||
- `requestUrl`: The url we want to fetch.
|
||||
- `options`: Please refer to this [documentation](https://github.com/github/fetch).
|
||||
- `true`: This third argument is optional. If true is passed the response will be sent only if the server has restarted check out the [example](#example-with-server-autoReload-watcher).
|
||||
|
||||
### Usage
|
||||
|
||||
**Path -** `/plugins/my-plugin/admin/src/containers/**/sagas.js`.
|
||||
|
||||
```js
|
||||
import { call, fork, put, takeLatest } from 'redux-saga/effects';
|
||||
|
||||
// Our request helper
|
||||
import request from 'utils/request';
|
||||
import { dataFetchSucceeded, dataFetchError } from './actions';
|
||||
import { DATA_FETCH } from './constants';
|
||||
|
||||
export function* fetchData(action) {
|
||||
try {
|
||||
const opts = {
|
||||
method: 'GET',
|
||||
};
|
||||
const requestUrl = `/my-plugin/${action.endPoint}`;
|
||||
const data = yield call(request, requestUrl, opts);
|
||||
|
||||
yield put(dataFetchSucceeded(data));
|
||||
} catch(error) {
|
||||
yield put(dataFetchError(error))
|
||||
}
|
||||
}
|
||||
|
||||
// Individual exports for testing
|
||||
function* defaultSaga() {
|
||||
yield fork(takeLatest, DATA_FETCH, fetchData);
|
||||
}
|
||||
|
||||
export default defaultSaga;
|
||||
```
|
||||
|
||||
### Simple example
|
||||
|
||||
Let's say that we have a container that fetches Content Type configurations depending on URL change.
|
||||
|
||||
#### Routing declaration:
|
||||
|
||||
Here we want to create a route `/content-type/:contentTypeName` for the `ContentTypePage` container.
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/container/App/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { Switch, Route, withRouter } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { pluginId } from 'app';
|
||||
|
||||
import ContentTypePage from 'containers/ContentTypePage';
|
||||
import styles from './styles.scss';
|
||||
|
||||
class App extends React.Component {
|
||||
render() {
|
||||
return (
|
||||
<div className={`${pluginId} ${styles.app}`}>
|
||||
<Switch>
|
||||
<Route exact path="/plugins/my-plugin/content-type/:contentTypeName" component={ContentTypePage} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
App.contextTypes = {
|
||||
router: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{},
|
||||
dispatch
|
||||
);
|
||||
}
|
||||
|
||||
const mapStateToProps = createStructuredSelector({});
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
|
||||
export default compose(
|
||||
withConnect,
|
||||
)(App);
|
||||
|
||||
```
|
||||
***
|
||||
|
||||
#### Constants declaration:
|
||||
|
||||
Let's declare the needed constants to handle fetching data:
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/ContentTypePage/constants.js`.
|
||||
```js
|
||||
export const DATA_FETCH = 'myPlugin/ContentTypePage/DATA_FETCH';
|
||||
export const DATA_FETCH_ERROR = 'myPlugin/ContentTypePage/DATA_FETCH_ERROR';
|
||||
export const DATA_FETCH_SUCCEEDED = 'myPlugin/ContentTypePage/DATA_FETCH_SUCCEEDED';
|
||||
```
|
||||
***
|
||||
|
||||
#### Actions declaration:
|
||||
|
||||
Let's declare our actions.
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/ContentTypePage/actions.js`.
|
||||
```js
|
||||
import {
|
||||
DATA_FETCH,
|
||||
DATA_FETCH_ERROR,
|
||||
DATA_FETCH_SUCCEEDED,
|
||||
} from './constants';
|
||||
|
||||
export function dataFetch(contentTypeName) {
|
||||
return {
|
||||
type: DATA_FETCH,
|
||||
contentTypeName,
|
||||
};
|
||||
}
|
||||
|
||||
export function dataFetchError(errorMessage) {
|
||||
return {
|
||||
type: DATA_FETCH_ERROR,
|
||||
errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
export function dataFetchSucceeded(data) {
|
||||
// data will look like { data: { name: 'User', description: 'Some description' } }
|
||||
return {
|
||||
type: DATA_FETCH_SUCCEEDED,
|
||||
data,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
#### Reducer setup:
|
||||
|
||||
Please refer to the [Immutable documentation](https://facebook.github.io/immutable-js/docs/#/) for informations about data structure.
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/ContentTypePage/reducer.js`.
|
||||
```js
|
||||
import { fromJS, Map } from 'immutable';
|
||||
import {
|
||||
DATA_FETCH,
|
||||
DATA_FETCH_ERROR,
|
||||
DATA_FETCH_SUCCEEDED
|
||||
} from './constants';
|
||||
|
||||
const initialState = fromJS({
|
||||
contentTypeName,
|
||||
error: false,
|
||||
errorMessage: '',
|
||||
data: Map({}),
|
||||
});
|
||||
|
||||
function contentTypePageReducer(state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case DATA_FETCH:
|
||||
return state.set('contentTypeName', action.contentTypeName);
|
||||
case DATA_FETCH_ERROR:
|
||||
return state
|
||||
.set('error', true)
|
||||
.set('errorMessage', action.errorMessage);
|
||||
case DATA_FETCH_SUCCEEDED:
|
||||
return state
|
||||
.set('error', false)
|
||||
.set('data', Map(action.data.data));
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export default contentTypePageReducer;
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
#### Selectors setup:
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/ContentTypePage/selectors.js`.
|
||||
```js
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
/**
|
||||
* Direct selector to the contentTypePage state domain
|
||||
*/
|
||||
const selectContentTypePageDomain = () => state => state.get('contentTypePage');
|
||||
|
||||
/**
|
||||
* Other specific selectors
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Default selector used by ContentTypePage
|
||||
*/
|
||||
|
||||
const selectContentTypePage = () => createSelector(
|
||||
selectContentTypePageDomain(),
|
||||
(substate) => substate.toJS()
|
||||
);
|
||||
|
||||
const makeSelectContentTypeName = () => createSelector(
|
||||
selectContentTypePageDomain(),
|
||||
(substate) => substate.get('contentTypeName');
|
||||
)
|
||||
export default selectContentTypePage;
|
||||
export { makeSelectContentTypeName, selectContentTypePageDomain };
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
#### Handling route change:
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/ContentTypePage/index.js`.
|
||||
```js
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { createStructuredSelector } from 'reselect';
|
||||
import { bindActionCreators, compose } from 'redux';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { map } from 'lodash';
|
||||
|
||||
// Utils to create the container's store
|
||||
import injectSaga from 'utils/injectSaga';
|
||||
import injectReducer from 'utils/injectReducer';
|
||||
|
||||
import { dataFetch } from './actions';
|
||||
import { selectContentTypePage } from './selectors';
|
||||
import saga from './sagas';
|
||||
import reducer from './reducer';
|
||||
import styles from './styles.scss';
|
||||
|
||||
export class ContentTypePage extends React.Component { // eslint-disable-line react/prefer-stateless-function
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.links = [
|
||||
{
|
||||
to: 'plugin/my-plugin/content-type/product',
|
||||
info: 'Product',
|
||||
},
|
||||
{
|
||||
to: 'plugin/my-plugin/content-type/user',
|
||||
info: 'User',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.props.dataFetch(this.props.match.params.contentTypeName);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
if (nextProps.match.params.contentTypeName !== this.props.match.params.contentTypeName) {
|
||||
this.props.dataFetch(nextProps.match.params.contentTypeName);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className={styles.contentTypePage}>
|
||||
<div>
|
||||
<ul>
|
||||
{map(this.links, (link, key) => (
|
||||
<li key={key}>
|
||||
<NavLink to={link.to}>{link.info}</NavLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h1>{this.props.data.name}</h1>
|
||||
<p>{this.props.data.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const mapStateToProps = selectContentTypePage();
|
||||
|
||||
function mapDispatchToProps(dispatch) {
|
||||
return bindActionCreators(
|
||||
{
|
||||
dataFetch,
|
||||
},
|
||||
dispatch,
|
||||
);
|
||||
}
|
||||
|
||||
ContentTypePage.propTypes = {
|
||||
data: PropTypes.object.isRequired,
|
||||
dataFetch: PropTypes.func.isRequired,
|
||||
match: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const withConnect = connect(mapStateToProps, mapDispatchToProps);
|
||||
const withSaga = injectSaga({ key: 'contentTypePage', saga });
|
||||
const withReducer = injectReducer({ key: 'contentTypePage', reducer });
|
||||
|
||||
export default compose(
|
||||
withReducer,
|
||||
withSaga,
|
||||
withConnect,
|
||||
)(ContentTypePage);
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
#### Fetching data:
|
||||
|
||||
The `sagas.js` file is in charge of fetching data.
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/ContentTypePage/sagas.js`.
|
||||
```js
|
||||
import { LOCATION_CHANGE } from 'react-router-redux';
|
||||
import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
|
||||
import request from 'utils/request';
|
||||
import {
|
||||
dataFetchError,
|
||||
dataFetchSucceeded,
|
||||
} from './actions';
|
||||
import { DATA_FETCH } from './constants';
|
||||
import { makeSelectContentTypeName } from './selectors';
|
||||
|
||||
export function* fetchData() {
|
||||
try {
|
||||
const opts = { method: 'GET' };
|
||||
|
||||
// To make a POST request { method: 'POST', body: {Object} }
|
||||
|
||||
const endPoint = yield select(makeSelectContentTypeName());
|
||||
const requestUrl = `my-plugin/**/${endPoint}`;
|
||||
|
||||
// Fetching data with our request helper
|
||||
const data = yield call(request, requestUrl, opts);
|
||||
yield put(dataFetchSucceeded(data));
|
||||
} catch(error) {
|
||||
yield put(dataFetchError(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
function* defaultSaga() {
|
||||
const loadDataWatcher = yield fork(takeLatest, DATA_FETCH, fetchData);
|
||||
|
||||
yield take(LOCATION_CHANGE);
|
||||
yield cancel(loadDataWatcher);
|
||||
}
|
||||
|
||||
export default defaultSaga;
|
||||
```
|
||||
|
||||
***
|
||||
***
|
||||
|
||||
### Example with server autoReload watcher
|
||||
|
||||
Let's say that you want to develop a plugin that needs server restart on file change (like the settings-manager plugin) and you want to be aware of that to display some stuff..., you just have to send a third argument: `true` to our request helper and it will ping a dedicated route and send the response when the server has restarted.
|
||||
|
||||
|
||||
**Path —** `./plugins/my-plugin/admin/src/containers/**/sagas.js`.
|
||||
```js
|
||||
import { takeLatest, call, take, put, fork, cancel, select } from 'redux-saga/effects';
|
||||
import request from 'utils/request';
|
||||
import {
|
||||
submitSucceeded,
|
||||
submitError,
|
||||
} from './actions';
|
||||
import { SUBMIT } from './constants';
|
||||
// Other useful imports like selectors...
|
||||
// ...
|
||||
|
||||
export function* postData() {
|
||||
try {
|
||||
const body = { data: 'someData' };
|
||||
const opts = { method: 'POST', body };
|
||||
const requestUrl = `**yourUrl**`;
|
||||
|
||||
const response = yield call(request, requestUrl, opts, true);
|
||||
|
||||
if (response.ok) {
|
||||
yield put(submitSucceeded());
|
||||
} else {
|
||||
yield put(submitError('An error occurred'));
|
||||
}
|
||||
} catch(error) {
|
||||
yield put(submitError(error.message));
|
||||
}
|
||||
}
|
||||
|
||||
function* defaultSaga() {
|
||||
yield fork(takeLatest, SUBMIT, postData);
|
||||
// ...
|
||||
}
|
||||
|
||||
export default defaultSaga;
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user