mirror of
				https://github.com/strapi/strapi.git
				synced 2025-10-31 01:47:13 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			472 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
			
		
		
	
	
			472 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Markdown
		
	
	
	
	
	
| # Helpers
 | |
| 
 | |
| Strapi provides helpers so you don't have to develop again and again the same generic functions.
 | |
| 
 | |
| ## Auth
 | |
| 
 | |
| `auth.js` lets you get, set and delete data in either the browser's `localStorage` or `sessionStorage`.
 | |
| 
 | |
| ### Methods
 | |
| 
 | |
| | Name | Description |
 | |
| | ---- | ----------- |
 | |
| | clear(key) | Remove the data in either `localStorage` or `sessionStorage` |
 | |
| | clearAppStorage() | Remove all data from both storage |
 | |
| | clearToken() | Remove the user's `jwt Token` in the appropriate browser's storage |
 | |
| | clearUserInfo() | Remove the user's info from storage |
 | |
| | get(key) | Get the item in the browser's storage |
 | |
| | getToken() | Get the user's `jwtToken` |
 | |
| | getUserInfo() | Get the user's infos |
 | |
| | set(value, key, isLocalStorage) | Set an item in the `sessionStorage`. If `true` is passed as the 3rd parameter it sets the value in the `localStorage` |
 | |
| | setToken(value, isLocalStorage) | Set the user's `jwtToken` in the `sessionStorage`. If `true` is passed as the 2nd parameter it sets the value in the `localStorage` |
 | |
| | setUserInfo(value, isLocalStorage) | Set the user's info in the `sessionStorage`. If `true` is passed as the 2nd parameter it sets the value in the `localStorage` |
 | |
| 
 | |
| 
 | |
| ```js
 | |
| import auth from 'utils/auth';
 | |
| 
 | |
| // ...
 | |
| //
 | |
| auth.setToken('12345', true); // This will set 1234 in the browser's localStorage associated with the key: jwtToken
 | |
| ```
 | |
| 
 | |
| ## Colors
 | |
| 
 | |
| This function allows to darken a color.
 | |
| 
 | |
| ### Usage
 | |
| 
 | |
| ```js
 | |
| import { darken } from 'utils/colors';
 | |
| 
 | |
| const linkColor = darken('#f5f5f5', 1.5); // Will darken #F5F5F5 by 1.5% which gives #f2f2f2.
 | |
| ```
 | |
| 
 | |
| ## Get URL Query Parameters
 | |
| 
 | |
| The helpers allows to retrieve the query parameters in the URL.
 | |
| 
 | |
| ### Example
 | |
| 
 | |
| ```js
 | |
| import getQueryParameters from 'utils/getQueryParameters';
 | |
| 
 | |
| const URL = '/create?source=users-permissions';
 | |
| const source = getQueryParameters(URL, 'source');
 | |
| 
 | |
| console.log(source); // users-permissions
 | |
| 
 | |
| ```
 | |
| 
 | |
| 
 | |
| ## 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;
 | |
| ```
 | 
