diff --git a/packages/core/admin/admin/src/StrapiApp.js b/packages/core/admin/admin/src/StrapiApp.js index bbe0ef2bad..36867db510 100644 --- a/packages/core/admin/admin/src/StrapiApp.js +++ b/packages/core/admin/admin/src/StrapiApp.js @@ -22,7 +22,7 @@ import { } from './exposedHooks'; import favicon from './favicon.png'; import injectionZones from './injectionZones'; -import App from './pages/App'; +import { App } from './pages/App'; import languageNativeNames from './translations/languageNativeNames'; class StrapiApp { diff --git a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js index 8a83736507..ae55ef90fb 100644 --- a/packages/core/admin/admin/src/components/AuthenticatedApp/index.js +++ b/packages/core/admin/admin/src/components/AuthenticatedApp/index.js @@ -27,7 +27,7 @@ import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion'; const strapiVersion = packageJSON.version; -const AuthenticatedApp = () => { +export const AuthenticatedApp = () => { const { setGuidedTourVisibility } = useGuidedTour(); const toggleNotification = useNotification(); const userInfo = auth.getUserInfo(); diff --git a/packages/core/admin/admin/src/index.js b/packages/core/admin/admin/src/index.js index 65efa9596f..4508b2171c 100644 --- a/packages/core/admin/admin/src/index.js +++ b/packages/core/admin/admin/src/index.js @@ -52,7 +52,7 @@ const run = async () => { // We need to make sure to fetch the project type before importing the StrapiApp // otherwise the strapi-babel-plugin does not work correctly - const StrapiApp = await import(/* webpackChunkName: "admin-app" */ './StrapiApp'); + const StrapiApp = await import(/* webpackChunkName: "StrapiApp" */ './StrapiApp'); const app = StrapiApp.default({ appPlugins: plugins, diff --git a/packages/core/admin/admin/src/pages/App/index.js b/packages/core/admin/admin/src/pages/App/index.js index 216c138011..67962ba2e0 100644 --- a/packages/core/admin/admin/src/pages/App/index.js +++ b/packages/core/admin/admin/src/pages/App/index.js @@ -4,7 +4,7 @@ * */ -import React, { lazy, Suspense, useEffect, useMemo, useState } from 'react'; +import * as React from 'react'; import { SkipToContent } from '@strapi/design-system'; import { @@ -12,31 +12,52 @@ import { LoadingIndicatorPage, prefixFileUrlWithBackendUrl, TrackingProvider, - useAppInfo, useFetchClient, - useNotification, } from '@strapi/helper-plugin'; import merge from 'lodash/merge'; import { useIntl } from 'react-intl'; +import { useQueries } from 'react-query'; import { useDispatch } from 'react-redux'; import { Route, Switch } from 'react-router-dom'; import PrivateRoute from '../../components/PrivateRoute'; import { ADMIN_PERMISSIONS_CE } from '../../constants'; -import { useConfigurations } from '../../hooks'; +import useConfigurations from '../../hooks/useConfigurations'; import { useEnterprise } from '../../hooks/useEnterprise'; -import { createRoute, makeUniqueRoutes } from '../../utils'; -import AuthPage from '../AuthPage'; -import NotFoundPage from '../NotFoundPage'; -import UseCasePage from '../UseCasePage'; +import createRoute from '../../utils/createRoute'; import { ROUTES_CE, SET_ADMIN_PERMISSIONS } from './constants'; -const AuthenticatedApp = lazy(() => - import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp') +const AuthPage = React.lazy(() => + import(/* webpackChunkName: "Admin-AuthPage" */ '../AuthPage').then((module) => ({ + default: module.AuthPage, + })) ); -function App() { +const AuthenticatedApp = React.lazy(() => + import(/* webpackChunkName: "Admin-AuthenticatedApp" */ '../../components/AuthenticatedApp').then( + (module) => ({ default: module.AuthenticatedApp }) + ) +); + +const UseCasePage = React.lazy(() => + import(/* webpackChunkName: "Admin-UseCasePage" */ '../UseCasePage').then((module) => ({ + default: module.UseCasePage, + })) +); + +const NotFoundPage = React.lazy(() => + import(/* webpackChunkName: "Admin_NotFoundPage" */ '../NotFoundPage').then((module) => ({ + default: module.NotFoundPage, + })) +); + +export function App() { + const { updateProjectSettings } = useConfigurations(); + const { formatMessage } = useIntl(); + const dispatch = useDispatch(); + const { get, post } = useFetchClient(); + const adminPermissions = useEnterprise( ADMIN_PERMISSIONS_CE, async () => (await import('../../../../ee/admin/constants')).ADMIN_PERMISSIONS_EE, @@ -49,6 +70,7 @@ function App() { defaultValue: ADMIN_PERMISSIONS_CE, } ); + const routes = useEnterprise( ROUTES_CE, async () => (await import('../../../../ee/admin/pages/App/constants')).ROUTES_EE, @@ -56,138 +78,158 @@ function App() { defaultValue: [], } ); - const toggleNotification = useNotification(); - const { updateProjectSettings } = useConfigurations(); - const { formatMessage } = useIntl(); - const [{ isLoading, hasAdmin, uuid, deviceId }, setState] = useState({ - isLoading: true, + + const [{ hasAdmin, uuid }, setState] = React.useState({ hasAdmin: false, + uuid: undefined, }); - const dispatch = useDispatch(); - const appInfo = useAppInfo(); - const { get, post } = useFetchClient(); - const authRoutes = useMemo(() => { - return makeUniqueRoutes( - routes.map(({ to, Component, exact }) => createRoute(Component, to, exact)) - ); - }, [routes]); - - const [telemetryProperties, setTelemetryProperties] = useState(null); - - useEffect(() => { + // Store permissions in redux + React.useEffect(() => { dispatch({ type: SET_ADMIN_PERMISSIONS, payload: adminPermissions }); }, [adminPermissions, dispatch]); - useEffect(() => { - const currentToken = auth.getToken(); - - const renewToken = async () => { - try { + const [ + { data: token, error: errorRenewToken }, + { data: initData, isLoading: isLoadingInit }, + { data: telemetryProperties }, + ] = useQueries([ + { + queryKey: 'renew-token', + async queryFn() { const { data: { data: { token }, }, - } = await post('/admin/renew-token', { token: currentToken }); - auth.updateToken(token); - } catch (err) { - // Refresh app - auth.clearAppStorage(); - window.location.reload(); - } - }; + } = await post('/admin/renew-token', { token: auth.getToken() }); - if (currentToken) { - renewToken(); - } - }, [post]); + return token; + }, - useEffect(() => { - const getData = async () => { - try { + enabled: !!auth.getToken(), + }, + + { + queryKey: 'init', + async queryFn() { const { - data: { - data: { hasAdmin, uuid, menuLogo, authLogo }, - }, + data: { data }, } = await get(`/admin/init`); - updateProjectSettings({ - menuLogo: prefixFileUrlWithBackendUrl(menuLogo), - authLogo: prefixFileUrlWithBackendUrl(authLogo), + return data; + }, + }, + + { + queryKey: 'telemetry-properties', + async queryFn() { + const { + data: { data }, + } = await get(`/admin/telemetry-properties`, { + // NOTE: needed because the interceptors of the fetchClient redirect to /login when receive a + // 401 and it would end up in an infinite loop when the user doesn't have a session. + validateStatus: (status) => status < 500, }); - if (uuid) { - const { - data: { data: properties }, - } = await get(`/admin/telemetry-properties`, { - // NOTE: needed because the interceptors of the fetchClient redirect to /login when receive a 401 and it would end up in an infinite loop when the user doesn't have a session. - validateStatus: (status) => status < 500, - }); + return data; + }, - setTelemetryProperties(properties); + enabled: !!auth.getToken(), + }, + ]); - try { - const event = 'didInitializeAdministration'; - await post( - 'https://analytics.strapi.io/api/v2/track', - { - // This event is anonymous - event, - userId: '', - deviceId, - eventPropeties: {}, - userProperties: { environment: appInfo.currentEnvironment }, - groupProperties: { ...properties, projectId: uuid }, - }, - { - headers: { - 'X-Strapi-Event': event, - }, - } - ); - } catch (e) { - // Silent. - } - } + React.useEffect(() => { + // If the renew token could not be fetched, logout the user + if (errorRenewToken) { + auth.clearAppStorage(); + window.location.reload(); + } else if (token) { + auth.updateToken(token); + } + }, [errorRenewToken, token]); - setState({ isLoading: false, hasAdmin, uuid, deviceId }); - } catch (err) { - toggleNotification({ - type: 'warning', - message: { id: 'app.containers.App.notification.error.init' }, - }); - } - }; + // Store the fetched project settings (e.g. logos) + // TODO: this should be moved to redux + React.useEffect(() => { + if (!isLoadingInit && initData) { + updateProjectSettings({ + menuLogo: prefixFileUrlWithBackendUrl(initData.menuLogo), + authLogo: prefixFileUrlWithBackendUrl(initData.authLogo), + }); - getData(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [toggleNotification, updateProjectSettings]); + // TODO: this should be stored in redux + setState((prev) => ({ + ...prev, + hasAdmin: initData.hasAdmin, + uuid: initData.uuid, + })); + } + }, [initData, isLoadingInit, updateProjectSettings]); - const setHasAdmin = (hasAdmin) => setState((prev) => ({ ...prev, hasAdmin })); + // we can't use useTracking here, because `App` is not wrapped in the tracking provider + // context. This shouldn't use `useFetchClient`, because it does not talk to the admin + // API + React.useEffect(() => { + async function trackInitEvent() { + await fetch('https://analytics.strapi.io/api/v2/track', { + body: JSON.stringify({ + event: 'didInitializeAdministration', + // This event is anonymous + userId: '', + eventPropeties: {}, + userProperties: {}, + groupProperties: { ...telemetryProperties, projectId: uuid }, + }), - const trackingInfo = useMemo( + headers: { + 'Content-Type': 'application/json', + 'X-Strapi-Event': 'didInitializeAdministration', + }, + + method: 'POST', + }); + } + + if (uuid) { + trackInitEvent(); + } + }, [telemetryProperties, uuid]); + + const authRoutes = routes + .map(({ to, Component, exact }) => createRoute(Component, to, exact)) + .filter( + (route, index, refArray) => refArray.findIndex((obj) => obj.key === route.key) === index + ); + + const trackingContext = React.useMemo( () => ({ uuid, telemetryProperties, - deviceId, }), - [uuid, telemetryProperties, deviceId] + [uuid, telemetryProperties] ); - if (isLoading) { + if (isLoadingInit) { return ; } return ( - }> + }> {formatMessage({ id: 'skipToContent' })} - + {authRoutes} ( - + { + // TODO: this should be a flag in redux + React.setState((prev) => ({ ...prev, hasAdmin })); + }} + hasAdmin={hasAdmin} + /> )} exact /> @@ -196,8 +238,6 @@ function App() { - + ); } - -export default App; diff --git a/packages/core/admin/admin/src/pages/AuthPage/index.js b/packages/core/admin/admin/src/pages/AuthPage/index.js index 0f3c6568f9..72f8d42244 100644 --- a/packages/core/admin/admin/src/pages/AuthPage/index.js +++ b/packages/core/admin/admin/src/pages/AuthPage/index.js @@ -18,7 +18,7 @@ import { FORMS } from './constants'; import init from './init'; import { initialState, reducer } from './reducer'; -const AuthPage = ({ hasAdmin, setHasAdmin }) => { +export const AuthPage = ({ hasAdmin, setHasAdmin }) => { const { push, location: { search }, @@ -315,5 +315,3 @@ AuthPage.propTypes = { hasAdmin: PropTypes.bool, setHasAdmin: PropTypes.func.isRequired, }; - -export default AuthPage; diff --git a/packages/core/admin/admin/src/pages/NotFoundPage/index.js b/packages/core/admin/admin/src/pages/NotFoundPage/index.js index a48d81b1b2..7d82f33447 100644 --- a/packages/core/admin/admin/src/pages/NotFoundPage/index.js +++ b/packages/core/admin/admin/src/pages/NotFoundPage/index.js @@ -11,7 +11,7 @@ import { LinkButton, useFocusWhenNavigate } from '@strapi/helper-plugin'; import { ArrowRight, EmptyPictures } from '@strapi/icons'; import { useIntl } from 'react-intl'; -const NoContentType = () => { +export const NotFoundPage = () => { const { formatMessage } = useIntl(); useFocusWhenNavigate(); @@ -46,5 +46,3 @@ const NoContentType = () => { ); }; - -export default NoContentType; diff --git a/packages/core/admin/admin/src/pages/NotFoundPage/tests/index.test.js b/packages/core/admin/admin/src/pages/NotFoundPage/tests/index.test.js index 028c91b616..cb47ebeed0 100644 --- a/packages/core/admin/admin/src/pages/NotFoundPage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/NotFoundPage/tests/index.test.js @@ -6,7 +6,7 @@ import { createMemoryHistory } from 'history'; import { IntlProvider } from 'react-intl'; import { Router } from 'react-router-dom'; -import NotFoundPage from '../index'; +import { NotFoundPage } from '../index'; const history = createMemoryHistory(); diff --git a/packages/core/admin/admin/src/pages/SettingsPage/index.js b/packages/core/admin/admin/src/pages/SettingsPage/index.js index a647420d44..7bf137fc53 100644 --- a/packages/core/admin/admin/src/pages/SettingsPage/index.js +++ b/packages/core/admin/admin/src/pages/SettingsPage/index.js @@ -8,8 +8,8 @@ import { Redirect, Route, Switch, useParams } from 'react-router-dom'; import { useSettingsMenu } from '../../hooks'; import { useEnterprise } from '../../hooks/useEnterprise'; -import createRoute from '../../utils/createRoute'; -import makeUniqueRoutes from '../../utils/makeUniqueRoutes'; +// eslint-disable-next-line +import { createRoute, makeUniqueRoutes } from '../../utils'; import SettingsNav from './components/SettingsNav'; import { ROUTES_CE } from './constants'; diff --git a/packages/core/admin/admin/src/pages/UseCasePage/index.js b/packages/core/admin/admin/src/pages/UseCasePage/index.js index 40e08fea9d..7b7de0f401 100644 --- a/packages/core/admin/admin/src/pages/UseCasePage/index.js +++ b/packages/core/admin/admin/src/pages/UseCasePage/index.js @@ -69,7 +69,7 @@ const TypographyCenter = styled(Typography)` text-align: center; `; -const UseCasePage = () => { +export const UseCasePage = () => { const toggleNotification = useNotification(); const { push, location } = useHistory(); const { formatMessage } = useIntl(); @@ -171,5 +171,3 @@ const UseCasePage = () => { ); }; - -export default UseCasePage; diff --git a/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js b/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js index e54abf0545..f8ce1ae3dd 100644 --- a/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js +++ b/packages/core/admin/admin/src/pages/UseCasePage/tests/index.test.js @@ -7,7 +7,7 @@ import { createMemoryHistory } from 'history'; import { IntlProvider } from 'react-intl'; import { Router } from 'react-router-dom'; -import UseCasePage from '../index'; +import { UseCasePage } from '../index'; jest.mock('../../../components/LocalesProvider/useLocalesProvider', () => () => ({ changeLocale() {}, diff --git a/packages/core/admin/admin/src/utils/index.js b/packages/core/admin/admin/src/utils/index.js index 4a2531048d..3dcbfd588a 100644 --- a/packages/core/admin/admin/src/utils/index.js +++ b/packages/core/admin/admin/src/utils/index.js @@ -4,6 +4,5 @@ export { default as formatAPIErrors } from './formatAPIErrors'; export { default as getAttributesToDisplay } from './getAttributesToDisplay'; export { default as getExistingActions } from './getExistingActions'; export { default as getFullName } from './getFullName'; -export { default as makeUniqueRoutes } from './makeUniqueRoutes'; export { default as sortLinks } from './sortLinks'; export { default as hashAdminUserEmail } from './uniqueAdminHash'; diff --git a/packages/core/admin/admin/src/utils/makeUniqueRoutes.js b/packages/core/admin/admin/src/utils/makeUniqueRoutes.js deleted file mode 100644 index c837b362f0..0000000000 --- a/packages/core/admin/admin/src/utils/makeUniqueRoutes.js +++ /dev/null @@ -1,6 +0,0 @@ -const makeUniqueRoutes = (routes) => - routes.filter((route, index, refArray) => { - return refArray.findIndex((obj) => obj.key === route.key) === index; - }); - -export default makeUniqueRoutes;