Load and display models in left menu

This commit is contained in:
Pierre Burgy 2017-03-18 17:34:00 +01:00
parent ef7ad188f2
commit e13c612509
21 changed files with 252 additions and 41 deletions

View File

@ -7,6 +7,13 @@
"config": {
"policies": []
}
}, {
"method": "GET",
"path": "/api/models",
"handler": "Admin.models",
"config": {
"policies": []
}
},
{
"method": "GET",

View File

@ -18,6 +18,10 @@ module.exports = {
}
},
models: async (ctx) => {
ctx.body = strapi.models;
},
file: async ctx => {
try {
const file = fs.readFileSync(path.resolve(__dirname, '..', 'public', 'build', ctx.params.file));

View File

@ -103,7 +103,7 @@ window.onload = function onLoad() {
// import { install } from 'offline-plugin/runtime';
// install();
import { pluginLoaded } from './containers/App/actions';
import { pluginLoaded, updatePlugin } from './containers/App/actions';
/**
* Public Strapi object exposed to the `window` object
@ -172,10 +172,13 @@ window.Strapi = {
},
port,
apiUrl,
refresh: () => ({
refresh: (pluginId) => ({
translationMessages: (translationMessagesUpdated) => {
render(_.merge({}, translationMessages, translationMessagesUpdated));
},
leftMenuLinks: (leftMenuLinksUpdated) => {
store.dispatch(updatePlugin(pluginId, 'leftMenuLinks', leftMenuLinksUpdated));
},
}),
};

View File

@ -1,8 +1,8 @@
/**
*
* LeftMenuLink
*
*/
*
* LeftMenuLink
*
*/
import React from 'react';
import styles from './styles.scss';
@ -12,8 +12,13 @@ import LeftMenuSubLinkContainer from 'components/LeftMenuSubLinkContainer';
class LeftMenuLink extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
let subLinksContainer;
if (this.props.leftMenuLinks && this.props.leftMenuLinks.length) {
subLinksContainer = <LeftMenuSubLinkContainer subLinks={this.props.leftMenuLinks} />;
if (this.props.leftMenuLinks && this.props.leftMenuLinks.size) {
subLinksContainer = (
<LeftMenuSubLinkContainer
subLinks={this.props.leftMenuLinks}
destinationPrefix={this.props.destination}
/>
);
}
return (
@ -33,7 +38,7 @@ LeftMenuLink.propTypes = {
label: React.PropTypes.string,
destination: React.PropTypes.string,
isActive: React.PropTypes.bool,
leftMenuLinks: React.PropTypes.array,
leftMenuLinks: React.PropTypes.object,
};
export default LeftMenuLink;

View File

@ -11,14 +11,14 @@ import styles from './styles.scss';
class LeftMenuLinkContainer extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
// List of links
let links = this.props.plugins.valueSeq().map(plugin => (
let links = this.props.plugins.valueSeq().map((plugin) => (
<LeftMenuLink
key={plugin.id}
icon={plugin.icon || 'ion-merge'}
label={plugin.name}
destination={`/plugins/${plugin.id}`}
isActive={this.props.params.plugin === plugin.id}
leftMenuLinks={plugin.leftMenuLinks}
key={plugin.get('id')}
icon={plugin.get('icon') || 'ion-merge'}
label={plugin.get('name')}
destination={`/plugins/${plugin.get('id')}`}
isActive={this.props.params.plugin === plugin.get('id')}
leftMenuLinks={plugin.get('leftMenuLinks')}
/>
));

View File

@ -14,8 +14,8 @@ class LeftMenuSubLinkContainer extends React.Component { // eslint-disable-line
let links = this.props.subLinks.map((subLink, i) => (
<LeftMenuSubLink
key={i}
label={subLink.label}
destination={`/plugins/${subLink.to}`}
label={subLink.get('label')}
destination={`${this.props.destinationPrefix}/${subLink.get('to')}`}
isActive={false}
/>
));
@ -29,7 +29,8 @@ class LeftMenuSubLinkContainer extends React.Component { // eslint-disable-line
}
LeftMenuSubLinkContainer.propTypes = {
subLinks: React.PropTypes.array,
subLinks: React.PropTypes.object,
destinationPrefix: React.PropTypes.string,
};
export default LeftMenuSubLinkContainer;

View File

@ -5,8 +5,9 @@
*/
import {
PLUGIN_LOADED,
LOAD_PLUGIN,
UPDATE_PLUGIN,
PLUGIN_LOADED,
} from './constants';
export function loadPlugin(newPlugin) {
@ -16,6 +17,15 @@ export function loadPlugin(newPlugin) {
};
}
export function updatePlugin(pluginId, updatedKey, updatedValue) {
return {
type: UPDATE_PLUGIN,
pluginId,
updatedKey,
updatedValue,
};
}
export function pluginLoaded(newPlugin) {
return {
type: PLUGIN_LOADED,

View File

@ -5,4 +5,5 @@
*/
export const LOAD_PLUGIN = 'app/App/LOAD_PLUGIN';
export const UPDATE_PLUGIN = 'app/App/UPDATE_PLUGIN';
export const PLUGIN_LOADED = 'app/App/PLUGIN_LOADED';

View File

@ -1,5 +1,6 @@
import { fromJS } from 'immutable';
import {
UPDATE_PLUGIN,
PLUGIN_LOADED,
} from './constants';
@ -10,7 +11,9 @@ const initialState = fromJS({
function appReducer(state = initialState, action) {
switch (action.type) {
case PLUGIN_LOADED:
return state.setIn(['plugins', action.plugin.id], action.plugin);
return state.setIn(['plugins', action.plugin.id], fromJS(action.plugin));
case UPDATE_PLUGIN:
return state.setIn(['plugins', action.pluginId, action.updatedKey], fromJS(action.updatedValue));
default:
return state;
}

View File

@ -23,7 +23,7 @@ export class PluginPage extends React.Component { // eslint-disable-line react/p
render() {
const containers = this.props.plugins.valueSeq().map((plugin, i) => {
const Elem = plugin.mainComponent;
const Elem = plugin.get('mainComponent');
return <Elem key={i} {...this.props} exposedComponents={exposedComponents}></Elem>;
});

View File

@ -7,6 +7,8 @@
import { browserHistory } from 'react-router';
import configureStore from './store';
import React from 'react';
import { Provider } from 'react-redux';
// Create redux store with history
// this uses the singleton browserHistory provided by react-router
@ -23,20 +25,24 @@ import { translationMessages } from './i18n';
// Plugin identifier based on the package.json `name` value
const pluginId = require('../package.json').name.replace(/^strapi-plugin-/i, '');
class comp extends React.Component { // eslint-disable-line react/prefer-stateless-function
render() {
return (
<Provider store={store}>
<App />
</Provider>
);
}
}
// Register the plugin
if (window.Strapi) {
window.Strapi.registerPlugin({
name: 'Content Manager',
icon: 'ion-document-text',
id: pluginId,
leftMenuLinks: [{
label: 'Articles',
to: 'content-manager/list/articles',
}, {
label: 'Categories',
to: 'content-manager/list/categories',
}],
mainComponent: App,
leftMenuLinks: [],
mainComponent: comp,
routes: createRoutes(store),
translationMessages,
});
@ -64,4 +70,5 @@ const apiUrl = window.Strapi && `${window.Strapi.apiUrl}/${pluginId}`;
export {
store,
apiUrl,
pluginId,
};

View File

@ -0,0 +1,23 @@
/*
*
* App actions
*
*/
import {
LOAD_MODELS,
LOADED_MODELS
} from './constants';
export function loadModels() {
return {
type: LOAD_MODELS,
};
}
export function loadedModels(models) {
return {
type: LOADED_MODELS,
models
};
}

View File

@ -0,0 +1,8 @@
/*
*
* App constants
*
*/
export const LOAD_MODELS = 'contentManager/App/LOAD_MODELS';
export const LOADED_MODELS = 'contentManager/App/LOADED_MODELS';

View File

@ -6,12 +6,18 @@
*/
import React from 'react';
import { Provider } from 'react-redux';
import { store } from '../../app';
import { createStructuredSelector } from 'reselect';
import { loadModels } from './actions';
import { makeSelectModels } from './selectors';
import { connect } from 'react-redux';
import '../../styles/main.scss';
export default class App extends React.Component { // eslint-disable-line react/prefer-stateless-function
class App extends React.Component { // eslint-disable-line react/prefer-stateless-function
componentWillMount() {
this.props.loadModels();
}
render() {
// Assign plugin component to children
const childrenWithProps = React.Children.map(this.props.children,
@ -21,11 +27,28 @@ export default class App extends React.Component { // eslint-disable-line react/
);
return (
<Provider store={store}>
<div className='content-manager'>
{React.Children.toArray(childrenWithProps)}
</div>
</Provider>
<div className='content-manager'>
{React.Children.toArray(childrenWithProps)}
</div>
);
}
}
App.propTypes = {
children: React.PropTypes.node,
loadModels: React.PropTypes.func,
};
export function mapDispatchToProps(dispatch) {
return {
loadModels: () => dispatch(loadModels()),
dispatch,
};
}
const mapStateToProps = createStructuredSelector({
models: makeSelectModels(),
});
// Wrap the component to inject dispatch and state into it
export default connect(mapStateToProps, mapDispatchToProps)(App);

View File

@ -0,0 +1,32 @@
/*
*
* List reducer
*
*/
import { fromJS } from 'immutable';
import {
LOAD_MODELS,
LOADED_MODELS,
} from './constants';
const initialState = fromJS({
loading: false,
models: {}
});
function appReducer(state = initialState, action) {
switch (action.type) {
case LOAD_MODELS:
return state
.set('loading', true);
case LOADED_MODELS:
return state
.set('loading', false)
.set('models', action.models);
default:
return state;
}
}
export default appReducer;

View File

@ -0,0 +1,46 @@
import { takeLatest } from 'redux-saga';
import { fork, put } from 'redux-saga/effects';
import _ from 'lodash';
import {
loadedModels
} from './actions';
import {
LOAD_MODELS,
} from './constants';
export function* getModels() {
try {
const opts = {
method: 'GET',
mode: 'cors',
cache: 'default'
};
const response = yield fetch('http://localhost:1337/admin/api/models', opts);
const data = yield response.json();
yield put(loadedModels(data));
const leftMenuLinks = _.map(data, (model, key) => ({
label: model.globalId,
to: key,
}));
// Update the admin left menu links
window.Strapi.refresh('content-manager').leftMenuLinks(leftMenuLinks);
} catch (err) {
console.error(err);
}
}
// Individual exports for testing
export function* defaultSaga() {
// yield takeLatest(LOAD_MODELS, getModels);
yield fork(takeLatest, LOAD_MODELS, getModels);
}
// All sagas to be loaded
export default [
defaultSaga,
];

View File

@ -0,0 +1,31 @@
import { createSelector } from 'reselect';
/**
* Direct selector to the list state domain
*/
const selectGlobalDomain = () => (state) => state.get('global');
/**
* Other specific selectors
*/
/**
* Default selector used by List
*/
const makeSelectModels = () => createSelector(
selectGlobalDomain(),
(globalState) => globalState.get('models'),
);
const makeSelectLoading = () => createSelector(
selectGlobalDomain(),
(substate) => substate.get('loading')
);
export {
selectGlobalDomain,
makeSelectLoading,
makeSelectModels,
};

View File

@ -19,7 +19,7 @@ import {
import {
makeSelectModelRecords,
makeSelectLoading
makeSelectLoading,
} from './selectors';
export class List extends React.Component { // eslint-disable-line react/prefer-stateless-function
@ -84,7 +84,7 @@ function mapDispatchToProps(dispatch) {
const mapStateToProps = createStructuredSelector({
records: makeSelectModelRecords(),
loading: makeSelectLoading()
loading: makeSelectLoading(),
});
export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(List));

View File

@ -30,7 +30,7 @@ export function* getRecords() {
mode: 'cors',
cache: 'default'
};
const response = yield fetch('http://localhost:1337/admin/config/models', opts);
const response = yield fetch('http://localhost:1337/admin/api/models', opts);
const data = yield response.json();
yield put(loadedRecord(fakeData, data));

View File

@ -7,6 +7,8 @@ import { combineReducers } from 'redux-immutable';
import { fromJS } from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
import globalReducer from 'containers/App/reducer';
/*
* routeReducer
*
@ -41,6 +43,7 @@ function routeReducer(state = routeInitialState, action) {
export default function createReducer(asyncReducers) {
return combineReducers({
route: routeReducer,
global: globalReducer,
...asyncReducers,
});
}

View File

@ -3,6 +3,7 @@
// See http://blog.mxstbr.com/2016/01/react-apps-with-pages for more information
// about the code splitting business
import { getAsyncInjectors } from 'utils/asyncInjectors';
import appSagas from 'containers/App/sagas';
const loadModule = (cb) => (componentModule) => {
cb(null, componentModule.default);
@ -12,6 +13,9 @@ export default function createRoutes(store) {
// Create reusable async injectors using getAsyncInjectors factory
const { injectReducer, injectSagas } = getAsyncInjectors(store); // eslint-disable-line no-unused-vars
// Inject app sagas
injectSagas(appSagas);
return [
{
path: '',