mirror of
https://github.com/strapi/strapi.git
synced 2025-09-27 09:25:46 +00:00
Chore: Dissolve AppLayout and PluginInitializer
This commit is contained in:
parent
9bdd5931a1
commit
e43368e10e
@ -7,20 +7,43 @@ import {
|
|||||||
useFetchClient,
|
useFetchClient,
|
||||||
useGuidedTour,
|
useGuidedTour,
|
||||||
useNotification,
|
useNotification,
|
||||||
|
useStrapiApp,
|
||||||
} from '@strapi/helper-plugin';
|
} from '@strapi/helper-plugin';
|
||||||
|
import produce from 'immer';
|
||||||
import { useQueries } from 'react-query';
|
import { useQueries } from 'react-query';
|
||||||
import { valid, lt } from 'semver';
|
import { valid, lt } from 'semver';
|
||||||
|
|
||||||
import packageJSON from '../../../package.json';
|
import packageJSON from '../../../package.json';
|
||||||
import { useConfigurations } from '../hooks';
|
import { useConfigurations } from '../hooks';
|
||||||
|
import { Admin } from '../pages/Admin';
|
||||||
import getFullName from '../utils/getFullName';
|
import getFullName from '../utils/getFullName';
|
||||||
import { hashAdminUserEmail } from '../utils/uniqueAdminHash';
|
import { hashAdminUserEmail } from '../utils/uniqueAdminHash';
|
||||||
|
|
||||||
import { PluginsInitializer } from './PluginsInitializer';
|
|
||||||
import RBACProvider from './RBACProvider';
|
import RBACProvider from './RBACProvider';
|
||||||
|
|
||||||
const strapiVersion = packageJSON.version;
|
const strapiVersion = packageJSON.version;
|
||||||
|
|
||||||
|
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': {
|
||||||
|
if (!draftState.plugins?.[action.pluginId]) {
|
||||||
|
draftState.plugins[action.pluginId] = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
draftState.plugins[action.pluginId].isReady = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return draftState;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const checkLatestStrapiVersion = (currentPackageVersion, latestPublishedVersion) => {
|
const checkLatestStrapiVersion = (currentPackageVersion, latestPublishedVersion) => {
|
||||||
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
|
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
|
||||||
return false;
|
return false;
|
||||||
@ -39,6 +62,13 @@ export const AuthenticatedApp = () => {
|
|||||||
const [userDisplayName, setUserDisplayName] = React.useState(userName);
|
const [userDisplayName, setUserDisplayName] = React.useState(userName);
|
||||||
const [userId, setUserId] = React.useState(null);
|
const [userId, setUserId] = React.useState(null);
|
||||||
const { showReleaseNotification } = useConfigurations();
|
const { showReleaseNotification } = useConfigurations();
|
||||||
|
const { plugins: appPlugins } = useStrapiApp();
|
||||||
|
const [{ plugins }, dispatch] = React.useReducer(reducer, initialState, () => ({
|
||||||
|
plugins: appPlugins,
|
||||||
|
}));
|
||||||
|
const setPlugin = React.useRef((pluginId) => {
|
||||||
|
dispatch({ type: 'SET_PLUGIN_READY', pluginId });
|
||||||
|
});
|
||||||
const [
|
const [
|
||||||
{ data: appInfos, isLoading: isLoadingAppInfos },
|
{ data: appInfos, isLoading: isLoadingAppInfos },
|
||||||
{ data: tagName, isLoading: isLoadingRelease },
|
{ data: tagName, isLoading: isLoadingRelease },
|
||||||
@ -135,8 +165,52 @@ export const AuthenticatedApp = () => {
|
|||||||
generateUserId();
|
generateUserId();
|
||||||
}, [userInfo]);
|
}, [userInfo]);
|
||||||
|
|
||||||
if (isLoadingRelease || isLoadingAppInfos || isLoadingPermissions) {
|
const hasApluginNotReady = Object.keys(plugins).some(
|
||||||
return <LoadingIndicatorPage />;
|
(plugin) => plugins[plugin].isReady === false
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* I have spent some time trying to understand what is happening here, and wanted to
|
||||||
|
* leave that knowledge for my future me:
|
||||||
|
*
|
||||||
|
* `initializer` is an undocumented property of the `registerPlugin` API. At the time
|
||||||
|
* of writing it seems only to be used by the i18n plugin.
|
||||||
|
*
|
||||||
|
* How does it work?
|
||||||
|
*
|
||||||
|
* Every plugin that has an `initializer` component defined, receives the
|
||||||
|
* `setPlugin` function as a component prop. In the case of i18n the plugin fetches locales
|
||||||
|
* first and calls `setPlugin` with `pluginId` once they are loaded, which then triggers the
|
||||||
|
* reducer of the admin app defined above.
|
||||||
|
*
|
||||||
|
* Once all plugins are set to `isReady: true` the app renders.
|
||||||
|
*
|
||||||
|
* This API is used to block rendering of the admin app. We should remove that in v5 completely
|
||||||
|
* and make sure plugins can inject data into the global store before they are initialized, to avoid
|
||||||
|
* having a new prop-callback based communication channel between plugins and the core admin app.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (isLoadingRelease || isLoadingAppInfos || isLoadingPermissions || 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 (
|
return (
|
||||||
@ -149,7 +223,7 @@ export const AuthenticatedApp = () => {
|
|||||||
userDisplayName={userDisplayName}
|
userDisplayName={userDisplayName}
|
||||||
>
|
>
|
||||||
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
|
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
|
||||||
<PluginsInitializer />
|
<Admin />
|
||||||
</RBACProvider>
|
</RBACProvider>
|
||||||
</AppInfoProvider>
|
</AppInfoProvider>
|
||||||
);
|
);
|
||||||
|
@ -1,66 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
import { LoadingIndicatorPage, useStrapiApp } from '@strapi/helper-plugin';
|
|
||||||
|
|
||||||
import { Admin } from '../../pages/Admin';
|
|
||||||
|
|
||||||
import init from './init';
|
|
||||||
import reducer, { initialState } from './reducer';
|
|
||||||
|
|
||||||
export const PluginsInitializer = () => {
|
|
||||||
const { plugins: appPlugins } = useStrapiApp();
|
|
||||||
const [{ plugins }, dispatch] = React.useReducer(reducer, initialState, () => init(appPlugins));
|
|
||||||
const setPlugin = React.useRef((pluginId) => {
|
|
||||||
dispatch({ type: 'SET_PLUGIN_READY', pluginId });
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasApluginNotReady = Object.keys(plugins).some(
|
|
||||||
(plugin) => plugins[plugin].isReady === false
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* I have spent some time trying to understand what is happening here, and wanted to
|
|
||||||
* leave that knowledge for my future me:
|
|
||||||
*
|
|
||||||
* `initializer` is an undocumented property of the `registerPlugin` API. At the time
|
|
||||||
* of writing it seems only to be used by the i18n plugin.
|
|
||||||
*
|
|
||||||
* How does it work?
|
|
||||||
*
|
|
||||||
* Every plugin that has an `initializer` component defined, receives the
|
|
||||||
* `setPlugin` function as a component prop. In the case of i18n the plugin fetches locales
|
|
||||||
* first and calls `setPlugin` with `pluginId` once they are loaded, which then triggers the
|
|
||||||
* reducer of the admin app defined above.
|
|
||||||
*
|
|
||||||
* Once all plugins are set to `isReady: true` the app renders.
|
|
||||||
*
|
|
||||||
* This API is used to block rendering of the admin app. We should remove that in v5 completely
|
|
||||||
* and make sure plugins can inject data into the global store before they are initialized, to avoid
|
|
||||||
* having a new prop-callback based communication channel between plugins and the core admin app.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
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 />;
|
|
||||||
};
|
|
@ -1,11 +0,0 @@
|
|||||||
const init = (plugins) => {
|
|
||||||
return {
|
|
||||||
plugins: Object.keys(plugins).reduce((acc, current) => {
|
|
||||||
acc[current] = { ...plugins[current] };
|
|
||||||
|
|
||||||
return acc;
|
|
||||||
}, {}),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export default init;
|
|
@ -1,22 +0,0 @@
|
|||||||
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;
|
|
@ -1,32 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { StrapiAppProvider } from '@strapi/helper-plugin';
|
|
||||||
import { render } from '@testing-library/react';
|
|
||||||
|
|
||||||
import { PluginsInitializer } from '../index';
|
|
||||||
|
|
||||||
jest.mock('../../../pages/Admin', () => () => {
|
|
||||||
return <div>ADMIN</div>;
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ADMIN | COMPONENTS | PluginsInitializer', () => {
|
|
||||||
it('should not crash', () => {
|
|
||||||
const getPlugin = jest.fn();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
render(
|
|
||||||
<StrapiAppProvider
|
|
||||||
plugins={{}}
|
|
||||||
getPlugin={getPlugin}
|
|
||||||
runHookParallel={jest.fn()}
|
|
||||||
runHookWaterfall={jest.fn()}
|
|
||||||
runHookSeries={jest.fn()}
|
|
||||||
menu={[]}
|
|
||||||
settings={{}}
|
|
||||||
>
|
|
||||||
<PluginsInitializer />
|
|
||||||
</StrapiAppProvider>
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,16 +0,0 @@
|
|||||||
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 });
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,40 +0,0 @@
|
|||||||
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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,33 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
|
|
||||||
import { Box, Flex, SkipToContent } from '@strapi/design-system';
|
|
||||||
import PropTypes from 'prop-types';
|
|
||||||
import { useIntl } from 'react-intl';
|
|
||||||
import styled from 'styled-components';
|
|
||||||
|
|
||||||
const FlexBox = styled(Box)`
|
|
||||||
flex: 1;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const AppLayout = ({ children, sideNav }) => {
|
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box background="neutral100">
|
|
||||||
<SkipToContent>
|
|
||||||
{formatMessage({ id: 'skipToContent', defaultMessage: 'Skip to content' })}
|
|
||||||
</SkipToContent>
|
|
||||||
<Flex alignItems="flex-start">
|
|
||||||
{sideNav}
|
|
||||||
<FlexBox>{children}</FlexBox>
|
|
||||||
</Flex>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
AppLayout.propTypes = {
|
|
||||||
children: PropTypes.node.isRequired,
|
|
||||||
sideNav: PropTypes.node.isRequired,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
|
import { Box, Flex } from '@strapi/design-system';
|
||||||
import { LoadingIndicatorPage, useStrapiApp, useTracking } from '@strapi/helper-plugin';
|
import { LoadingIndicatorPage, useStrapiApp, useTracking } from '@strapi/helper-plugin';
|
||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
@ -15,7 +16,6 @@ import { Route, Switch } from 'react-router-dom';
|
|||||||
import LeftMenu from '../../components/LeftMenu';
|
import LeftMenu from '../../components/LeftMenu';
|
||||||
import useConfigurations from '../../hooks/useConfigurations';
|
import useConfigurations from '../../hooks/useConfigurations';
|
||||||
import useMenu from '../../hooks/useMenu';
|
import useMenu from '../../hooks/useMenu';
|
||||||
import AppLayout from '../../layouts/AppLayout';
|
|
||||||
import createRoute from '../../utils/createRoute';
|
import createRoute from '../../utils/createRoute';
|
||||||
import { SET_APP_RUNTIME_STATUS } from '../App/constants';
|
import { SET_APP_RUNTIME_STATUS } from '../App/constants';
|
||||||
|
|
||||||
@ -82,33 +82,33 @@ export const Admin = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DndProvider backend={HTML5Backend}>
|
<DndProvider backend={HTML5Backend}>
|
||||||
<AppLayout
|
<Flex alignItems="stretch">
|
||||||
sideNav={
|
<LeftMenu
|
||||||
<LeftMenu
|
generalSectionLinks={generalSectionLinks}
|
||||||
generalSectionLinks={generalSectionLinks}
|
pluginsSectionLinks={pluginsSectionLinks}
|
||||||
pluginsSectionLinks={pluginsSectionLinks}
|
/>
|
||||||
/>
|
|
||||||
}
|
<Box flex="1">
|
||||||
>
|
<React.Suspense fallback={<LoadingIndicatorPage />}>
|
||||||
<React.Suspense fallback={<LoadingIndicatorPage />}>
|
<Switch>
|
||||||
<Switch>
|
<Route path="/" component={HomePage} exact />
|
||||||
<Route path="/" component={HomePage} exact />
|
<Route path="/me" component={ProfilePage} exact />
|
||||||
<Route path="/me" component={ProfilePage} exact />
|
<Route path="/content-manager" component={CM} />
|
||||||
<Route path="/content-manager" component={CM} />
|
{routes}
|
||||||
{routes}
|
<Route path="/settings/:settingId" component={SettingsPage} />
|
||||||
<Route path="/settings/:settingId" component={SettingsPage} />
|
<Route path="/settings" component={SettingsPage} exact />
|
||||||
<Route path="/settings" component={SettingsPage} exact />
|
<Route path="/marketplace">
|
||||||
<Route path="/marketplace">
|
<MarketplacePage />
|
||||||
<MarketplacePage />
|
</Route>
|
||||||
</Route>
|
<Route path="/list-plugins" exact>
|
||||||
<Route path="/list-plugins" exact>
|
<InstalledPluginsPage />
|
||||||
<InstalledPluginsPage />
|
</Route>
|
||||||
</Route>
|
<Route path="/404" component={NotFoundPage} />
|
||||||
<Route path="/404" component={NotFoundPage} />
|
<Route path="/500" component={InternalErrorPage} />
|
||||||
<Route path="/500" component={InternalErrorPage} />
|
<Route path="" component={NotFoundPage} />
|
||||||
<Route path="" component={NotFoundPage} />
|
</Switch>
|
||||||
</Switch>
|
</React.Suspense>
|
||||||
</React.Suspense>
|
</Box>
|
||||||
|
|
||||||
{/* TODO: we should move the logic to determine whether the guided tour is displayed
|
{/* TODO: we should move the logic to determine whether the guided tour is displayed
|
||||||
or not out of the component, to make the code-splitting more effective
|
or not out of the component, to make the code-splitting more effective
|
||||||
@ -116,7 +116,7 @@ export const Admin = () => {
|
|||||||
<GuidedTourModal />
|
<GuidedTourModal />
|
||||||
|
|
||||||
{showTutorials && <Onboarding />}
|
{showTutorials && <Onboarding />}
|
||||||
</AppLayout>
|
</Flex>
|
||||||
</DndProvider>
|
</DndProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user