mirror of
https://github.com/strapi/strapi.git
synced 2025-09-26 00:39:49 +00:00
Chore: Dissolve AppLayout and PluginInitializer
This commit is contained in:
parent
9bdd5931a1
commit
e43368e10e
@ -7,20 +7,43 @@ import {
|
||||
useFetchClient,
|
||||
useGuidedTour,
|
||||
useNotification,
|
||||
useStrapiApp,
|
||||
} from '@strapi/helper-plugin';
|
||||
import produce from 'immer';
|
||||
import { useQueries } from 'react-query';
|
||||
import { valid, lt } from 'semver';
|
||||
|
||||
import packageJSON from '../../../package.json';
|
||||
import { useConfigurations } from '../hooks';
|
||||
import { Admin } from '../pages/Admin';
|
||||
import getFullName from '../utils/getFullName';
|
||||
import { hashAdminUserEmail } from '../utils/uniqueAdminHash';
|
||||
|
||||
import { PluginsInitializer } from './PluginsInitializer';
|
||||
import RBACProvider from './RBACProvider';
|
||||
|
||||
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) => {
|
||||
if (!valid(currentPackageVersion) || !valid(latestPublishedVersion)) {
|
||||
return false;
|
||||
@ -39,6 +62,13 @@ export const AuthenticatedApp = () => {
|
||||
const [userDisplayName, setUserDisplayName] = React.useState(userName);
|
||||
const [userId, setUserId] = React.useState(null);
|
||||
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 [
|
||||
{ data: appInfos, isLoading: isLoadingAppInfos },
|
||||
{ data: tagName, isLoading: isLoadingRelease },
|
||||
@ -135,8 +165,52 @@ export const AuthenticatedApp = () => {
|
||||
generateUserId();
|
||||
}, [userInfo]);
|
||||
|
||||
if (isLoadingRelease || isLoadingAppInfos || isLoadingPermissions) {
|
||||
return <LoadingIndicatorPage />;
|
||||
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 (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 (
|
||||
@ -149,7 +223,7 @@ export const AuthenticatedApp = () => {
|
||||
userDisplayName={userDisplayName}
|
||||
>
|
||||
<RBACProvider permissions={permissions} refetchPermissions={refetch}>
|
||||
<PluginsInitializer />
|
||||
<Admin />
|
||||
</RBACProvider>
|
||||
</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 { Box, Flex } from '@strapi/design-system';
|
||||
import { LoadingIndicatorPage, useStrapiApp, useTracking } from '@strapi/helper-plugin';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||
@ -15,7 +16,6 @@ import { Route, Switch } from 'react-router-dom';
|
||||
import LeftMenu from '../../components/LeftMenu';
|
||||
import useConfigurations from '../../hooks/useConfigurations';
|
||||
import useMenu from '../../hooks/useMenu';
|
||||
import AppLayout from '../../layouts/AppLayout';
|
||||
import createRoute from '../../utils/createRoute';
|
||||
import { SET_APP_RUNTIME_STATUS } from '../App/constants';
|
||||
|
||||
@ -82,33 +82,33 @@ export const Admin = () => {
|
||||
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<AppLayout
|
||||
sideNav={
|
||||
<LeftMenu
|
||||
generalSectionLinks={generalSectionLinks}
|
||||
pluginsSectionLinks={pluginsSectionLinks}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<React.Suspense fallback={<LoadingIndicatorPage />}>
|
||||
<Switch>
|
||||
<Route path="/" component={HomePage} exact />
|
||||
<Route path="/me" component={ProfilePage} exact />
|
||||
<Route path="/content-manager" component={CM} />
|
||||
{routes}
|
||||
<Route path="/settings/:settingId" component={SettingsPage} />
|
||||
<Route path="/settings" component={SettingsPage} exact />
|
||||
<Route path="/marketplace">
|
||||
<MarketplacePage />
|
||||
</Route>
|
||||
<Route path="/list-plugins" exact>
|
||||
<InstalledPluginsPage />
|
||||
</Route>
|
||||
<Route path="/404" component={NotFoundPage} />
|
||||
<Route path="/500" component={InternalErrorPage} />
|
||||
<Route path="" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
<Flex alignItems="stretch">
|
||||
<LeftMenu
|
||||
generalSectionLinks={generalSectionLinks}
|
||||
pluginsSectionLinks={pluginsSectionLinks}
|
||||
/>
|
||||
|
||||
<Box flex="1">
|
||||
<React.Suspense fallback={<LoadingIndicatorPage />}>
|
||||
<Switch>
|
||||
<Route path="/" component={HomePage} exact />
|
||||
<Route path="/me" component={ProfilePage} exact />
|
||||
<Route path="/content-manager" component={CM} />
|
||||
{routes}
|
||||
<Route path="/settings/:settingId" component={SettingsPage} />
|
||||
<Route path="/settings" component={SettingsPage} exact />
|
||||
<Route path="/marketplace">
|
||||
<MarketplacePage />
|
||||
</Route>
|
||||
<Route path="/list-plugins" exact>
|
||||
<InstalledPluginsPage />
|
||||
</Route>
|
||||
<Route path="/404" component={NotFoundPage} />
|
||||
<Route path="/500" component={InternalErrorPage} />
|
||||
<Route path="" component={NotFoundPage} />
|
||||
</Switch>
|
||||
</React.Suspense>
|
||||
</Box>
|
||||
|
||||
{/* 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
|
||||
@ -116,7 +116,7 @@ export const Admin = () => {
|
||||
<GuidedTourModal />
|
||||
|
||||
{showTutorials && <Onboarding />}
|
||||
</AppLayout>
|
||||
</Flex>
|
||||
</DndProvider>
|
||||
);
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user