Use bindActionCreators, solve i18n issues

This commit is contained in:
Aurelsicoko 2017-09-14 11:10:05 +02:00
parent 6b6f1be98c
commit be4c757f5a
23 changed files with 140 additions and 124 deletions

View File

@ -12,7 +12,7 @@ import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { ConnectedRouter } from 'react-router-redux';
import createHistory from 'history/createBrowserHistory';
import _ from 'lodash';
import { merge, isFunction } from 'lodash';
import 'sanitize.css/sanitize.css';
import LanguageProvider from 'containers/LanguageProvider';
@ -77,14 +77,19 @@ window.onload = function onLoad() {
* @param params
*/
const registerPlugin = (plugin) => {
const formattedPlugin = plugin;
// Merge admin translation messages
_.merge(translationMessages, formattedPlugin.translationMessages);
merge(translationMessages, plugin.translationMessages);
formattedPlugin.leftMenuSections = formattedPlugin.leftMenuSections || [];
plugin.leftMenuSections = plugin.leftMenuSections || [];
store.dispatch(pluginLoaded(formattedPlugin));
// Execute bootstrap function.
if (isFunction(plugin.bootstrap)) {
plugin.bootstrap(plugin).then(plugin => {
store.dispatch(pluginLoaded(plugin));
});
} else {
store.dispatch(pluginLoaded(plugin));
}
};
const displayNotification = (message, status) => {
@ -114,7 +119,8 @@ window.Strapi = {
apiUrl,
refresh: (pluginId) => ({
translationMessages: (translationMessagesUpdated) => {
render(_.merge({}, translationMessages, translationMessagesUpdated));
console.log(translationMessagesUpdated);
render(merge({}, translationMessages, translationMessagesUpdated));
},
leftMenuSections: (leftMenuSectionsUpdated) => {
store.dispatch(updatePlugin(pluginId, 'leftMenuSections', leftMenuSectionsUpdated));

View File

@ -21,7 +21,14 @@ class LeftMenuLink extends React.Component { // eslint-disable-line react/prefer
<li className={styles.item}>
<Link className={`${styles.link} ${isLinkActive ? styles.linkActive : ''}`} to={this.props.destination}>
<i className={`${styles.linkIcon} fa-${this.props.icon} fa`}></i>
<FormattedMessage id={this.props.label} className={styles.linkLabel} />
<FormattedMessage
id={this.props.label}
defaultMessage='{label}'
values={{
label: this.props.label,
}}
className={styles.linkLabel}
/>
</Link>
</li>
);

View File

@ -26,7 +26,7 @@ import { hideNotification } from 'containers/NotificationProvider/actions';
import Header from 'components/Header/index';
import styles from './syles.scss';
import styles from './styles.scss';
export class AdminPage extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {

View File

@ -36,10 +36,6 @@ export class PluginPage extends React.Component { // eslint-disable-line react/p
}
}
PluginPage.contextTypes = {
router: React.PropTypes.object.isRequired,
};
PluginPage.propTypes = {
match: React.PropTypes.object.isRequired,
plugins: React.PropTypes.object.isRequired,

View File

@ -8,7 +8,7 @@
import React from 'react';
import { Provider } from 'react-redux';
import App from 'containers/App'; // eslint-disable-line
import App, { bootstrap } from 'containers/App'; // eslint-disable-line
import configureStore from './store';
import { translationMessages } from './i18n';
@ -27,7 +27,6 @@ const router = window.Strapi.router;
// Create redux store with Strapi admin history
const store = configureStore({}, window.Strapi.router);
// Define the plugin root component
function Comp(props) {
return (
@ -37,11 +36,6 @@ function Comp(props) {
);
}
// Add contextTypes to get access to the admin router
Comp.contextTypes = {
router: React.PropTypes.object.isRequired.isRequired,
};
// Hot reloadable translation json files
if (module.hot) {
// modules.hot.accept does not accept dynamic dependencies,
@ -58,7 +52,7 @@ if (module.hot) {
});
}
// Register the plugin
// Register the plugin.
window.Strapi.registerPlugin({
name: pluginPkg.strapi.name,
icon: pluginPkg.strapi.icon,
@ -66,8 +60,8 @@ window.Strapi.registerPlugin({
leftMenuLinks: [],
mainComponent: Comp,
translationMessages,
bootstrap,
});
// Export store
export { store, apiUrl, pluginId, pluginName, pluginDescription, router };

View File

@ -15,7 +15,10 @@ class Button extends React.Component {
const addShape = this.props.addShape ? <i className="fa fa-plus" /> : '';
return (
<button className={`${styles[this.props.buttonSize]} ${styles[this.props.buttonBackground]} ${styles.button}`} {...this.props}>
<button
className={`${styles[this.props.buttonSize]} ${styles[this.props.buttonBackground]} ${styles.button}`}
onClick={this.props.onClick}
>
{addShape}{label}
</button>
);

View File

@ -146,7 +146,7 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
<div className={`${styles.inputCheckbox} col-md-12 ${requiredClass}`}>
<div className="form-check">
{title}
<FormattedMessage id={this.props.label}>
<FormattedMessage id={this.props.label} defaultMessage={this.props.label}>
{(message) => (
<label className={`${styles.checkboxLabel} form-check-label`} htmlFor={this.props.label}>
<input className="form-check-input" type="checkbox" defaultChecked={this.props.value} onChange={this.handleChangeCheckbox} name={this.props.name} />
@ -168,7 +168,7 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
return (
<div className={`${styles.input} ${requiredClass} ${bootStrapClass}`}>
<label htmlFor={this.props.label}>
<FormattedMessage id={`${this.props.label}`} />
<FormattedMessage id={`${this.props.label}`} defaultMessage={this.props.label} />
</label>
<select
className="form-control"
@ -179,7 +179,7 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
disabled={this.props.disabled}
>
{map(this.props.selectOptions, (option, key) => (
<FormattedMessage id={`${option.name}`} key={key}>
<FormattedMessage id={`${option.name}`} defaultMessage={option.name} key={key}>
{(message) => (
<option value={option.value}>
{message}
@ -207,9 +207,9 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
return (
<div className={`${styles.inputTextArea} ${bootStrapClass} ${requiredClass} ${bootStrapClassDanger}`}>
<label htmlFor={this.props.label}>
<FormattedMessage id={`${this.props.label}`} />
<FormattedMessage id={`${this.props.label}`} defaultMessage={this.props.label} />
</label>
<FormattedMessage id={this.props.placeholder || this.props.label}>
<FormattedMessage id={this.props.placeholder || this.props.label} defaultMessage={this.props.label}>
{(placeholder) => (
<textarea
className="form-control"
@ -247,7 +247,7 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
return (
<div className={`${styles.input} ${bootStrapClass} ${requiredClass}`}>
<label htmlFor={this.props.label}>
<FormattedMessage id={`${this.props.label}`} />
<FormattedMessage id={`${this.props.label}`} defaultMessage={this.props.label} />
</label>
<DateTime
value={value}
@ -275,7 +275,7 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
}
renderFormattedInput = (handleBlur, inputValue, placeholder) => (
<FormattedMessage id={`${placeholder}`}>
<FormattedMessage id={`${placeholder}`} defaultMessage={placeholder}>
{(message) => (
<input
name={this.props.name}
@ -305,7 +305,7 @@ class Input extends React.Component { // eslint-disable-line react/prefer-statel
const placeholder = this.props.placeholder || this.props.label;
const label = this.props.label ?
<label htmlFor={this.props.label}><FormattedMessage id={`${this.props.label}`} /></label>
<label htmlFor={this.props.label}><FormattedMessage id={`${this.props.label}`} defaultMessage={this.props.label} /></label>
: <label htmlFor={this.props.label} />;
const requiredClass = get(this.props.validations, 'required') && this.props.addRequiredInputDesign ?

View File

@ -17,7 +17,7 @@ class PluginHeaderActions extends React.Component { // eslint-disable-line react
{...action}
key={action.label}
>
<FormattedMessage id={action.label} />
<FormattedMessage id={action.label} defaultMessage={action.label} />
</Button>
));

View File

@ -14,7 +14,7 @@ class PluginHeaderTitle extends React.Component { // eslint-disable-line react/p
return (
<div>
<h1 className={styles.pluginHeaderTitleName}>
<FormattedMessage {...this.props.title} />
<FormattedMessage {...this.props.title} defaultMessage={this.props.title.id} />
</h1>
<p className={styles.pluginHeaderTitleDescription}>
<FormattedMessage {...this.props.description} />

View File

@ -8,7 +8,11 @@ import 'whatwg-fetch';
* @return {object} The parsed JSON from the request
*/
function parseJSON(response) {
return response.json();
if (response.json) {
return response.json();
}
return response;
}
/**

View File

@ -116,4 +116,4 @@
"devDependencies": {
"babel-plugin-add-module-exports": "^0.2.1"
}
}
}

View File

@ -18,7 +18,7 @@ class TableFooter extends React.Component {
<div className="col-lg-6">
<LimitSelect
className="push-lg-right"
onLimitChange={this.props.onLimitChange}
handleLimit={this.props.handleLimit}
limit={this.props.limit}
/>
</div>
@ -42,8 +42,8 @@ TableFooter.propTypes = {
React.PropTypes.bool,
]).isRequired,
currentPage: React.PropTypes.number.isRequired,
handleLimit: React.PropTypes.func.isRequired,
limit: React.PropTypes.number.isRequired,
onLimitChange: React.PropTypes.func.isRequired,
};
export default TableFooter;

View File

@ -21,7 +21,7 @@ import List from 'containers/List';
import { loadModels, updateSchema } from './actions';
import { makeSelectLoading } from './selectors';
import saga from './sagas';
import saga, { generateMenu } from './sagas';
const tryRequire = (path) => {
try {
@ -31,8 +31,19 @@ const tryRequire = (path) => {
}
};
// This method is executed before the load of the plugin.
export const bootstrap = (plugin) => new Promise((resolve, reject) => {
generateMenu()
.then(menu => {
plugin.leftMenuSections = menu;
resolve(plugin);
})
.catch(e => reject(e));
});
class App extends React.Component {
componentWillMount() {
componentDidMount() {
const config = tryRequire('../../../../config/admin.json');
if (!_.isEmpty(_.get(config, 'admin.schema'))) {
@ -82,7 +93,6 @@ const mapStateToProps = createStructuredSelector({
});
const withConnect = connect(mapStateToProps, mapDispatchToProps);
const withSaga = injectSaga({ key: 'global', saga });
export default compose(

View File

@ -5,9 +5,30 @@ import request from 'utils/request';
import { generateSchema } from 'utils/schema';
import { loadedModels, updateSchema } from './actions';
import { LOAD_MODELS, LOADED_MODELS, UPDATE_SCHEMA } from './constants';
import { LOAD_MODELS, LOADED_MODELS } from './constants';
import { makeSelectModels } from './selectors';
export const generateMenu = function () {
try {
return request(`${window.Strapi.apiUrl}/content-manager/models`, {
method: 'GET',
})
.then(response => generateSchema(response))
.then(displayedModels => {
return [{
name: 'Content Types',
links: _.map(displayedModels, (model, key) => ({
label: model.labelPlural || model.label || key,
destination: key,
})),
}];
});
} catch (err) {
window.Strapi.notification.error(
'An error occurred during models config fetch.'
);
}
}
export function* getModels() {
try {
@ -40,27 +61,9 @@ export function* modelsLoaded() {
yield put(updateSchema(schema));
}
export function* schemaUpdated(action) {
// Display the links only if the `displayed` attribute is not set to false
const displayedModels = _.pickBy(action.schema, model => (model.displayed !== false));
// Map links to format them
const leftMenuSections = [{
name: 'Content Types',
links: _.map(displayedModels, (model, key) => ({
label: model.labelPlural || model.label || key,
destination: key,
})),
}];
// Update the admin left menu links
window.Strapi.refresh('content-manager').leftMenuSections(leftMenuSections);
}
// Individual exports for testing
export function* defaultSaga() {
yield fork(takeLatest, LOAD_MODELS, getModels);
yield fork(takeLatest, UPDATE_SCHEMA, schemaUpdated);
yield fork(takeLatest, LOADED_MODELS, modelsLoaded);
}

View File

@ -8,7 +8,7 @@
import React from 'react';
import moment from 'moment';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import { get, isObject } from 'lodash';
import { router } from 'app';
@ -37,7 +37,6 @@ import {
loadRecord,
setRecordAttribute,
editRecord,
deleteRecord,
toggleNull,
} from './actions';
@ -57,7 +56,11 @@ import reducer from './reducer';
import saga from './sagas';
export class Edit extends React.Component {
componentWillMount() {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.setInitialState();
this.props.setCurrentModelName(this.props.match.params.slug.toLowerCase());
@ -65,10 +68,15 @@ export class Edit extends React.Component {
if (this.props.match.params.id === 'create') {
this.props.setIsCreating();
} else {
console.log("LOAD RECORD");
this.props.loadRecord(this.props.match.params.id);
}
}
componentWillUnmount() {
console.log("UNMOUNTED");
}
handleChange = (e) => {
if (isObject(e.target.value) && e.target.value._isAMomentObject === true) {
e.target.value = moment(e.target.value, 'YYYY-MM-DD HH:mm:ss').format();
@ -237,25 +245,18 @@ const mapStateToProps = createStructuredSelector({
});
function mapDispatchToProps(dispatch) {
return {
setInitialState: () => dispatch(setInitialState()),
setCurrentModelName: currentModelName =>
dispatch(setCurrentModelName(currentModelName)),
setIsCreating: () => dispatch(setIsCreating()),
loadRecord: id => dispatch(loadRecord(id)),
setRecordAttribute: (key, value) =>
dispatch(setRecordAttribute(key, value)),
editRecord: () => dispatch(editRecord()),
deleteRecord: () => {
// TODO: improve confirmation UX.
if (window.confirm('Are you sure ?')) {
// eslint-disable-line no-alert
dispatch(deleteRecord());
}
return bindActionCreators(
{
setInitialState,
setCurrentModelName,
setIsCreating,
loadRecord,
setRecordAttribute,
editRecord,
toggleNull,
},
toggleNull: () => dispatch(toggleNull()),
dispatch,
};
dispatch
);
}
const withConnect = connect(mapStateToProps, mapDispatchToProps);

View File

@ -26,7 +26,7 @@ export function* getRecord(params) {
const requestUrl = `${window.Strapi.apiUrl}/content-manager/explorer/${currentModelName}/${params.id}`;
// Call our request helper (see 'utils/request')
const response = yield call(request, requestUrl, {
const response = yield request(requestUrl, {
method: 'GET',
});
@ -110,14 +110,11 @@ export function* deleteRecord({ id, modelName }) {
export function* defaultSaga() {
const loadRecordWatcher = yield fork(takeLatest, LOAD_RECORD, getRecord);
const editRecordWatcher = yield fork(takeLatest, EDIT_RECORD, editRecord);
const deleteRecordWatcher = yield fork(
takeLatest,
DELETE_RECORD,
deleteRecord
);
const deleteRecordWatcher = yield fork(takeLatest, DELETE_RECORD, deleteRecord);
// Suspend execution until location changes
yield take(LOCATION_CHANGE);
yield cancel(loadRecordWatcher);
yield cancel(editRecordWatcher);
yield cancel(deleteRecordWatcher);

View File

@ -65,6 +65,7 @@ export function changePage(page) {
}
export function changeSort(sort) {
console.log("COUCOU", sort);
return {
type: CHANGE_SORT,
sort,

View File

@ -6,7 +6,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { compose } from 'redux';
import { bindActionCreators, compose } from 'redux';
import { createStructuredSelector } from 'reselect';
import _ from 'lodash';
@ -65,17 +65,14 @@ export class List extends React.Component {
};
}
componentWillMount() {
componentDidMount() {
// Init the view
this.init(this.props.match.params.slug);
}
componentWillReceiveProps(nextProps) {
// Check if the current slug changed in the url
const locationChanged =
nextProps.location.pathname !== this.props.location.pathname;
const locationChanged = nextProps.location.pathname !== this.props.location.pathname;
// If the location changed, init the view
if (locationChanged) {
this.init(nextProps.match.params.slug);
}
@ -85,7 +82,6 @@ export class List extends React.Component {
// Set current model name
this.props.setCurrentModelName(slug.toLowerCase());
// Set default sort value
this.props.changeSort(this.props.models[slug.toLowerCase()].primaryKey || 'desc');
// Load records
@ -102,7 +98,7 @@ export class List extends React.Component {
e.preventDefault();
e.stopPropagation();
this.props.deleteRecord(this.state.target, this.props.currentModelName);
this.props.deleteRecord(this.state, this.props.currentModelName);
this.setState({ showWarning: false });
}
@ -207,7 +203,7 @@ export class List extends React.Component {
changePage={this.props.changePage}
count={this.props.count}
className="push-lg-right"
onLimitChange={this.props.onLimitChange}
handleLimit={this.props.changeLimit}
/>
</div>
</div>
@ -222,6 +218,7 @@ List.contextTypes = {
};
List.propTypes = {
changeLimit: React.PropTypes.func.isRequired,
changePage: React.PropTypes.func.isRequired,
changeSort: React.PropTypes.func.isRequired,
count: React.PropTypes.oneOfType([
@ -245,7 +242,6 @@ List.propTypes = {
React.PropTypes.object,
React.PropTypes.bool,
]).isRequired,
onLimitChange: React.PropTypes.func.isRequired,
records: React.PropTypes.oneOfType([
React.PropTypes.array,
React.PropTypes.bool,
@ -260,29 +256,18 @@ List.propTypes = {
};
function mapDispatchToProps(dispatch) {
return {
deleteRecord: (id, modelName) => dispatch(deleteRecord(id, modelName)),
setCurrentModelName: modelName => dispatch(setCurrentModelName(modelName)),
loadRecords: () => dispatch(loadRecords()),
loadCount: () => dispatch(loadCount()),
changePage: page => {
dispatch(changePage(page));
dispatch(loadRecords());
dispatch(loadCount());
return bindActionCreators(
{
deleteRecord,
setCurrentModelName,
loadRecords,
loadCount,
changePage,
changeSort,
changeLimit,
},
changeSort: sort => {
dispatch(changeSort(sort));
dispatch(loadRecords());
},
onLimitChange: e => {
const newLimit = Number(e.target.value);
dispatch(changeLimit(newLimit));
dispatch(changePage(1));
dispatch(loadRecords());
e.target.blur();
},
dispatch,
};
dispatch
);
}
const mapStateToProps = createStructuredSelector({

View File

@ -26,11 +26,13 @@ import {
} from './selectors';
export function* getRecords() {
console.log("GET RECORDS");
const currentModel = yield select(makeSelectCurrentModelName());
const limit = yield select(makeSelectLimit());
const currentPage = yield select(makeSelectCurrentPage());
const sort = yield select(makeSelectSort());
console.log(sort);
console.log("#1");
// Calculate the number of values to be skip
const skip = (currentPage - 1) * limit;
@ -43,14 +45,17 @@ export function* getRecords() {
try {
const requestUrl = `${window.Strapi.apiUrl}/content-manager/explorer/${currentModel}`;
console.log("#1.5");
// Call our request helper (see 'utils/request')
const response = yield call(request, requestUrl, {
method: 'GET',
params,
});
console.log("#2");
yield put(loadedRecord(response));
console.log("#3");
} catch (err) {
window.Strapi.notification.error('An error occurred during records fetch.');
}
@ -81,6 +86,7 @@ export function* defaultSaga() {
// Suspend execution until location changes
yield take(LOCATION_CHANGE);
yield cancel(loadRecordsWatcher);
yield cancel(loudCountWatcher);
yield cancel(deleteRecordWatcher);

View File

@ -52,15 +52,19 @@ function formatQueryParams(params) {
* @return {object} The response data
*/
export default function request(url, options) {
console.log(url, options);
const optionsObj = options || {};
// Set headers
optionsObj.headers = {
'Content-Type': 'application/json',
'Content-Type': 'text/plain',
};
// Add parameters to url
let urlFormatted = url;
let urlFormatted = _.startsWith(url, '/')
? `${Strapi.apiUrl}${url}`
: url;
if (optionsObj && optionsObj.params) {
const params = formatQueryParams(optionsObj.params);
urlFormatted = `${url}?${params}`;

View File

@ -1,7 +1,6 @@
module.exports = {
find: async function (params) {
console.log(params);
const entries = await this
.forge()
.query((qb) => {

View File

@ -64,4 +64,4 @@
"strapi-helper-plugin": "file:../strapi-helper-plugin",
"webpack": "^3.5.5"
}
}
}