Load plugin documentation and associated translations

Signed-off-by: soupette <cyril.lpz@gmail.com>
This commit is contained in:
soupette 2021-05-10 12:43:21 +02:00
parent 5dbb3776cc
commit 37e70cdcdf
19 changed files with 236 additions and 143 deletions

View File

@ -5,7 +5,7 @@ import { QueryClientProvider, QueryClient } from 'react-query';
import { ThemeProvider } from 'styled-components';
import { StrapiProvider } from '@strapi/helper-plugin';
import configureStore from './core/store/configureStore';
import { Middlewares, Plugin } from './core/apis';
import { Plugin } from './core/apis';
import basename from './utils/basename';
import App from './pages/App';
import LanguageProvider from './components/LanguageProvider';
@ -19,7 +19,7 @@ import themes from './themes';
import reducers from './reducers';
// TODO
import translationMessages from './translations';
import translations from './translations';
window.strapi = {
backendURL: process.env.STRAPI_ADMIN_BACKEND_URL,
@ -33,14 +33,22 @@ const queryClient = new QueryClient({
},
});
// FIXME
const appLocales = Object.keys(translations);
class StrapiApp {
constructor({ appPlugins }) {
this.translationMessages = translations;
this.appPlugins = appPlugins || {};
this.middlewares = Middlewares();
this.middlewares = [];
this.plugins = {};
this.reducers = { ...reducers };
}
addMiddleware(middleware) {
this.middlewares.push(middleware);
}
addReducers(reducers) {
Object.keys(reducers).forEach(reducerName => {
this.reducers[reducerName] = reducers[reducerName];
@ -51,22 +59,60 @@ class StrapiApp {
Object.keys(this.appPlugins).forEach(plugin => {
this.appPlugins[plugin].register(this);
});
return this;
}
async boot() {
console.log('booting');
Object.keys(this.appPlugins).forEach(plugin => {
const boot = this.appPlugins[plugin].boot;
return this;
if (boot) {
boot(this);
}
});
}
getPlugin(pluginId) {
return this.plugins[pluginId] || null;
}
// FIXME
registerPluginTranslations(pluginId, trads) {
const pluginTranslations = appLocales.reduce((acc, currentLanguage) => {
const currentLocale = trads[currentLanguage];
if (currentLocale) {
const localeprefixedWithPluginId = Object.keys(currentLocale).reduce((acc2, current) => {
acc2[`${pluginId}.${current}`] = currentLocale[current];
return acc2;
}, {});
acc[currentLanguage] = localeprefixedWithPluginId;
}
return acc;
}, {});
this.translationMessages = Object.keys(this.translationMessages).reduce((acc, current) => {
acc[current] = {
...this.translationMessages[current],
...(pluginTranslations[current] || {}),
};
return acc;
}, {});
}
registerPlugin(pluginConf) {
this.plugins[pluginConf.id] = Plugin(pluginConf);
const plugin = Plugin(pluginConf);
this.plugins[plugin.pluginId] = plugin;
// FIXME
// Translations should be loaded differently
// This is a temporary fix
this.registerPluginTranslations(plugin.pluginId, plugin.trads);
}
render() {
@ -79,7 +125,7 @@ class StrapiApp {
<Fonts />
<Provider store={store}>
<StrapiProvider strapi={this}>
<LanguageProvider messages={translationMessages}>
<LanguageProvider messages={this.translationMessages}>
<>
<AutoReloadOverlayBlocker />
<OverlayBlocker />

View File

@ -0,0 +1,41 @@
import React, { useReducer, useRef } from 'react';
import { LoadingIndicatorPage, useStrapi } from '@strapi/helper-plugin';
import Admin from '../../pages/Admin';
import init from './init';
import reducer, { initialState } from './reducer';
const PluginsInitializer = () => {
// TODO rename strapi to avoid mismatch with the window.strapi
const { strapi: app } = useStrapi();
const [{ plugins }, dispatch] = useReducer(reducer, initialState, () => init(app.plugins));
const setPlugin = useRef(pluginId => {
dispatch({ type: 'SET_PLUGIN_READY', pluginId });
});
const hasApluginNotReady = Object.keys(plugins).some(plugin => plugins[plugin].isReady === false);
if (hasApluginNotReady) {
const initializers = Object.keys(plugins).reduce((acc, current) => {
const InitializerComponent = plugins[current].initializer;
if (InitializerComponent) {
const key = plugins[current].pluginId;
acc.push(<InitializerComponent key={key} setPlugin={setPlugin.current} />);
}
return acc;
}, []);
return (
<>
{initializers}
<LoadingIndicatorPage />
</>
);
}
return <Admin plugins={plugins} />;
};
export default PluginsInitializer;

View File

@ -0,0 +1,11 @@
const init = plugins => {
return {
plugins: Object.keys(plugins).reduce((acc, current) => {
acc[current] = { ...plugins[current] };
return acc;
}, {}),
};
};
export default init;

View File

@ -0,0 +1,22 @@
import produce from 'immer';
import set from 'lodash/set';
const initialState = {
plugins: null,
};
const reducer = (state = initialState, action) =>
/* eslint-disable-next-line consistent-return */
produce(state, draftState => {
switch (action.type) {
case 'SET_PLUGIN_READY': {
set(draftState, ['plugins', action.pluginId, 'isReady'], true);
break;
}
default:
return draftState;
}
});
export { initialState };
export default reducer;

View File

@ -0,0 +1,18 @@
import React from 'react';
import { StrapiProvider } from '@strapi/helper-plugin';
import { render } from '@testing-library/react';
import PluginsInitializer from '../index';
jest.mock('../../../pages/Admin', () => () => <div>ADMIN</div>);
describe('ADMIN | COMPONENTS | PluginsInitializer', () => {
it('should not crash', () => {
expect(
render(
<StrapiProvider strapi={{ plugins: {} }}>
<PluginsInitializer />
</StrapiProvider>
)
);
});
});

View File

@ -0,0 +1,16 @@
import init from '../init';
describe('ADMIN | COMPONENT | PluginsInitializer | init', () => {
it('should return the initialState', () => {
const plugins = {
pluginA: {
isReady: false,
},
pluginB: {
isReady: false,
},
};
expect(init(plugins)).toEqual({ plugins });
});
});

View File

@ -0,0 +1,40 @@
import reducer, { initialState } from '../reducer';
describe('ADMIN | COMPONENTS | PluginsInitializer | reducer', () => {
let state;
beforeEach(() => {
state = initialState;
});
describe('DEFAULT_ACTION', () => {
it('should return the initialState', () => {
expect(reducer(state, {})).toEqual(initialState);
});
});
describe('SET_PLUGIN_READY', () => {
it('should set the isReady property to true for a plugin', () => {
state = {
plugins: {
pluginA: {
isReady: false,
},
},
};
const expected = {
plugins: {
pluginA: { isReady: true },
},
};
const action = {
type: 'SET_PLUGIN_READY',
pluginId: 'pluginA',
};
expect(reducer(state, action)).toEqual(expected);
});
});
});

View File

@ -1,15 +0,0 @@
import cloneDeep from 'lodash/cloneDeep';
class Middlewares {
middlewares = [];
add(middleware) {
this.middlewares.push(middleware);
}
get middlewares() {
return cloneDeep(this.middlewares);
}
}
export default () => new Middlewares();

View File

@ -5,8 +5,9 @@ class Plugin {
this.description = pluginConf.description;
// TODO
this.icon = pluginConf.icon;
this.initializer = pluginConf.initializer || null;
this.injectionZones = pluginConf.injectionZones || {};
this.isReady = pluginConf.isReady || true;
this.isReady = pluginConf.isReady !== undefined ? pluginConf.isReady : true;
// TODO
this.isRequired = pluginConf.isRequired;
// TODO

View File

@ -1,2 +1,2 @@
export { default as Middlewares } from './Middlewares';
// eslint-disable-next-line import/prefer-default-export
export { default as Plugin } from './Plugin';

View File

@ -2,9 +2,12 @@ import { createStore, applyMiddleware } from 'redux';
import createReducer from './createReducer';
const configureStore = app => {
// TODO
const middlewares = [];
middlewares.forEach(middleware => {
middlewares.push(middleware());
});
return createStore(createReducer(app.reducers), {}, applyMiddleware(...middlewares));
};

View File

@ -2,8 +2,6 @@ import ReactDOM from 'react-dom';
import StrapiApp from './StrapiApp';
import plugins from './plugins';
console.log({ plugins });
const app = StrapiApp({ appPlugins: plugins });
const MOUNT_NODE = document.getElementById('app');

View File

@ -17,7 +17,6 @@ import { isEmpty } from 'lodash';
import {
difference,
GlobalContextProvider,
LoadingIndicatorPage,
CheckPagePermissions,
request,
} from '@strapi/helper-plugin';
@ -149,45 +148,11 @@ export class Admin extends React.Component {
}
};
hasApluginNotReady = props => {
const {
global: { plugins },
} = props;
return !Object.keys(plugins).every(plugin => plugins[plugin].isReady === true);
};
initApp = async () => {
await this.fetchAppInfo();
await this.fetchStrapiLatestRelease();
};
/**
* Display the app loader until the app is ready
* @returns {Boolean}
*/
showLoader = () => {
return this.hasApluginNotReady(this.props);
};
renderInitializers = () => {
const {
global: { plugins },
} = this.props;
return Object.keys(plugins).reduce((acc, current) => {
const InitializerComponent = plugins[current].initializer;
if (InitializerComponent) {
const key = plugins[current].id;
acc.push(<InitializerComponent key={key} {...this.props} {...this.helpers} />);
}
return acc;
}, []);
};
renderPluginDispatcher = props => {
// NOTE: Send the needed props instead of everything...
@ -203,22 +168,14 @@ export class Admin extends React.Component {
render() {
const {
admin: { shouldUpdateStrapi },
global: { autoReload, currentEnvironment, plugins, strapiVersion },
global: { autoReload, currentEnvironment, strapiVersion },
// FIXME
intl: { formatMessage, locale },
// FIXME
plugins,
updatePlugin,
} = this.props;
// We need the admin data in order to make the initializers work
if (this.showLoader()) {
return (
<>
{this.renderInitializers()}
<LoadingIndicatorPage />
</>
);
}
return (
<PermissionsManager>
<GlobalContextProvider
@ -297,7 +254,6 @@ Admin.propTypes = {
global: PropTypes.shape({
autoReload: PropTypes.bool,
currentEnvironment: PropTypes.string,
plugins: PropTypes.object,
strapiVersion: PropTypes.string,
uuid: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
}).isRequired,
@ -305,7 +261,7 @@ Admin.propTypes = {
formatMessage: PropTypes.func,
locale: PropTypes.string,
}),
location: PropTypes.object.isRequired,
plugins: PropTypes.object.isRequired,
setAppError: PropTypes.func.isRequired,
updatePlugin: PropTypes.func.isRequired,
};

View File

@ -24,12 +24,12 @@ describe('<Admin />', () => {
getUserPermissions: jest.fn(),
getUserPermissionsError: jest.fn(),
getUserPermissionsSucceeded: jest.fn(),
plugins: {},
global: {
autoReload: false,
currentEnvironment: 'development',
hasAdminUser: false,
isLoading: true,
plugins: {},
strapiVersion: '3',
uuid: false,
},

View File

@ -10,8 +10,8 @@ import { Switch, Route } from 'react-router-dom';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { LoadingIndicatorPage, auth, request } from '@strapi/helper-plugin';
import PluginsInitializer from '../../components/PluginsInitializer';
import PrivateRoute from '../../components/PrivateRoute';
import Admin from '../Admin';
import AuthPage from '../AuthPage';
import NotFoundPage from '../NotFoundPage';
import { getUID } from './utils';
@ -117,7 +117,7 @@ function App(props) {
)}
exact
/>
<PrivateRoute path="/" component={Admin} />
<PrivateRoute path="/" component={PluginsInitializer} />
<Route path="" component={NotFoundPage} />
</Switch>
</Content>

View File

@ -14,7 +14,7 @@ import PageTitle from '../../components/PageTitle';
export function PluginDispatcher(props) {
const {
global: { plugins },
plugins,
match: {
params: { pluginId },
},
@ -53,12 +53,12 @@ export function PluginDispatcher(props) {
PluginDispatcher.defaultProps = {};
PluginDispatcher.propTypes = {
global: PropTypes.object.isRequired,
match: PropTypes.shape({
params: PropTypes.shape({
pluginId: PropTypes.string,
}),
}).isRequired,
plugins: PropTypes.object.isRequired,
};
export default memo(PluginDispatcher);

View File

@ -9,7 +9,7 @@ const Email = () => <div>Email Plugin</div>;
describe('<PluginDispatcher />', () => {
it('Should return null if the params does not match the pluginId', () => {
const props = {
global: { plugins: {} },
plugins: {},
match: { params: { pluginId: 'email' } },
};
@ -20,13 +20,11 @@ describe('<PluginDispatcher />', () => {
it('Should return the BlockerComponent if the plugin preventRendering prop is true', () => {
const props = {
global: {
plugins: {
email: {
mainComponent: Email,
preventComponentRendering: true,
blockerComponent: null,
},
plugins: {
email: {
mainComponent: Email,
preventComponentRendering: true,
blockerComponent: null,
},
},
match: { params: { pluginId: 'email' } },
@ -39,13 +37,11 @@ describe('<PluginDispatcher />', () => {
it('Should return a custom BlockerComponent if the plugin preventRendering prop is true and a custom blocker is given', () => {
const props = {
global: {
plugins: {
email: {
mainComponent: Email,
preventComponentRendering: true,
blockerComponent: BlockerComponent2,
},
plugins: {
email: {
mainComponent: Email,
preventComponentRendering: true,
blockerComponent: BlockerComponent2,
},
},
match: { params: { pluginId: 'email' } },
@ -58,11 +54,9 @@ describe('<PluginDispatcher />', () => {
it("Should return the plugin's mainComponent if all conditions are met", () => {
const props = {
global: {
plugins: {
email: {
mainComponent: Email,
},
plugins: {
email: {
mainComponent: Email,
},
},
match: { params: { pluginId: 'email' } },

View File

@ -1,15 +1,15 @@
import globalReducer from './pages/App/reducer';
import adminReducer from './pages/Admin/reducer';
import languageProviderReducer from './components/LanguageProvider/reducer';
import permissionsManagerReducer from './components/PermissionsManager/reducer';
import menuReducer from './components/LeftMenu/reducer';
import permissionsManagerReducer from './components/PermissionsManager/reducer';
const reducers = {
app: globalReducer,
admin: adminReducer,
app: globalReducer,
language: languageProviderReducer,
permissionsManager: permissionsManagerReducer,
menu: menuReducer,
permissionsManager: permissionsManagerReducer,
};
export default reducers;

View File

@ -4,7 +4,6 @@
// Here's the file: strapi/docs/3.0.0-beta.x/guides/registering-a-field-in-admin.md
// Also the strapi-generate-plugins/files/admin/src/index.js needs to be updated
// IF THE DOC IS NOT UPDATED THE PULL REQUEST WILL NOT BE MERGED
import pluginPkg from '../../package.json';
import pluginPermissions from './permissions';
import pluginId from './pluginId';
@ -12,43 +11,6 @@ import pluginLogo from './assets/images/logo.svg';
import App from './containers/App';
import trads from './translations';
// export default strapi => {
// const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
// const icon = pluginPkg.strapi.icon;
// const name = pluginPkg.strapi.name;
// const plugin = {
// blockerComponent: null,
// blockerComponentProps: {},
// description: pluginDescription,
// icon,
// id: pluginId,
// injectedComponents: [],
// isReady: true,
// isRequired: pluginPkg.strapi.required || false,
// mainComponent: App,
// name,
// pluginLogo,
// preventComponentRendering: false,
// trads,
// menu: {
// pluginsSectionLinks: [
// {
// destination: `/plugins/${pluginId}`,
// icon,
// label: {
// id: `${pluginId}.plugin.name`,
// defaultMessage: 'Documentation',
// },
// name,
// permissions: pluginPermissions.main,
// },
// ],
// },
// };
// return strapi.registerPlugin(plugin);
// };
const pluginDescription = pluginPkg.strapi.description || pluginPkg.description;
const icon = pluginPkg.strapi.icon;
const name = pluginPkg.strapi.name;