mirror of
https://github.com/strapi/strapi.git
synced 2025-09-20 14:00:48 +00:00
Chore: Refactor app entry file
This commit is contained in:
parent
0b2975dbed
commit
a23be30700
@ -22,7 +22,7 @@ import {
|
|||||||
} from './exposedHooks';
|
} from './exposedHooks';
|
||||||
import favicon from './favicon.png';
|
import favicon from './favicon.png';
|
||||||
import injectionZones from './injectionZones';
|
import injectionZones from './injectionZones';
|
||||||
import App from './pages/App';
|
import { App } from './pages/App';
|
||||||
import languageNativeNames from './translations/languageNativeNames';
|
import languageNativeNames from './translations/languageNativeNames';
|
||||||
|
|
||||||
class StrapiApp {
|
class StrapiApp {
|
||||||
|
@ -27,7 +27,7 @@ import checkLatestStrapiVersion from './utils/checkLatestStrapiVersion';
|
|||||||
|
|
||||||
const strapiVersion = packageJSON.version;
|
const strapiVersion = packageJSON.version;
|
||||||
|
|
||||||
const AuthenticatedApp = () => {
|
export const AuthenticatedApp = () => {
|
||||||
const { setGuidedTourVisibility } = useGuidedTour();
|
const { setGuidedTourVisibility } = useGuidedTour();
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const userInfo = auth.getUserInfo();
|
const userInfo = auth.getUserInfo();
|
||||||
|
@ -52,7 +52,7 @@ const run = async () => {
|
|||||||
|
|
||||||
// We need to make sure to fetch the project type before importing the StrapiApp
|
// We need to make sure to fetch the project type before importing the StrapiApp
|
||||||
// otherwise the strapi-babel-plugin does not work correctly
|
// 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({
|
const app = StrapiApp.default({
|
||||||
appPlugins: plugins,
|
appPlugins: plugins,
|
||||||
|
@ -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 { SkipToContent } from '@strapi/design-system';
|
||||||
import {
|
import {
|
||||||
@ -12,31 +12,52 @@ import {
|
|||||||
LoadingIndicatorPage,
|
LoadingIndicatorPage,
|
||||||
prefixFileUrlWithBackendUrl,
|
prefixFileUrlWithBackendUrl,
|
||||||
TrackingProvider,
|
TrackingProvider,
|
||||||
useAppInfo,
|
|
||||||
useFetchClient,
|
useFetchClient,
|
||||||
useNotification,
|
|
||||||
} from '@strapi/helper-plugin';
|
} from '@strapi/helper-plugin';
|
||||||
import merge from 'lodash/merge';
|
import merge from 'lodash/merge';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
import { useQueries } from 'react-query';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { Route, Switch } from 'react-router-dom';
|
import { Route, Switch } from 'react-router-dom';
|
||||||
|
|
||||||
import PrivateRoute from '../../components/PrivateRoute';
|
import PrivateRoute from '../../components/PrivateRoute';
|
||||||
import { ADMIN_PERMISSIONS_CE } from '../../constants';
|
import { ADMIN_PERMISSIONS_CE } from '../../constants';
|
||||||
import { useConfigurations } from '../../hooks';
|
import useConfigurations from '../../hooks/useConfigurations';
|
||||||
import { useEnterprise } from '../../hooks/useEnterprise';
|
import { useEnterprise } from '../../hooks/useEnterprise';
|
||||||
import { createRoute, makeUniqueRoutes } from '../../utils';
|
import createRoute from '../../utils/createRoute';
|
||||||
import AuthPage from '../AuthPage';
|
|
||||||
import NotFoundPage from '../NotFoundPage';
|
|
||||||
import UseCasePage from '../UseCasePage';
|
|
||||||
|
|
||||||
import { ROUTES_CE, SET_ADMIN_PERMISSIONS } from './constants';
|
import { ROUTES_CE, SET_ADMIN_PERMISSIONS } from './constants';
|
||||||
|
|
||||||
const AuthenticatedApp = lazy(() =>
|
const AuthPage = React.lazy(() =>
|
||||||
import(/* webpackChunkName: "Admin-authenticatedApp" */ '../../components/AuthenticatedApp')
|
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(
|
const adminPermissions = useEnterprise(
|
||||||
ADMIN_PERMISSIONS_CE,
|
ADMIN_PERMISSIONS_CE,
|
||||||
async () => (await import('../../../../ee/admin/constants')).ADMIN_PERMISSIONS_EE,
|
async () => (await import('../../../../ee/admin/constants')).ADMIN_PERMISSIONS_EE,
|
||||||
@ -49,6 +70,7 @@ function App() {
|
|||||||
defaultValue: ADMIN_PERMISSIONS_CE,
|
defaultValue: ADMIN_PERMISSIONS_CE,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const routes = useEnterprise(
|
const routes = useEnterprise(
|
||||||
ROUTES_CE,
|
ROUTES_CE,
|
||||||
async () => (await import('../../../../ee/admin/pages/App/constants')).ROUTES_EE,
|
async () => (await import('../../../../ee/admin/pages/App/constants')).ROUTES_EE,
|
||||||
@ -56,138 +78,158 @@ function App() {
|
|||||||
defaultValue: [],
|
defaultValue: [],
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
const toggleNotification = useNotification();
|
|
||||||
const { updateProjectSettings } = useConfigurations();
|
const [{ hasAdmin, uuid }, setState] = React.useState({
|
||||||
const { formatMessage } = useIntl();
|
|
||||||
const [{ isLoading, hasAdmin, uuid, deviceId }, setState] = useState({
|
|
||||||
isLoading: true,
|
|
||||||
hasAdmin: false,
|
hasAdmin: false,
|
||||||
|
uuid: undefined,
|
||||||
});
|
});
|
||||||
const dispatch = useDispatch();
|
|
||||||
const appInfo = useAppInfo();
|
|
||||||
const { get, post } = useFetchClient();
|
|
||||||
|
|
||||||
const authRoutes = useMemo(() => {
|
// Store permissions in redux
|
||||||
return makeUniqueRoutes(
|
React.useEffect(() => {
|
||||||
routes.map(({ to, Component, exact }) => createRoute(Component, to, exact))
|
|
||||||
);
|
|
||||||
}, [routes]);
|
|
||||||
|
|
||||||
const [telemetryProperties, setTelemetryProperties] = useState(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
dispatch({ type: SET_ADMIN_PERMISSIONS, payload: adminPermissions });
|
dispatch({ type: SET_ADMIN_PERMISSIONS, payload: adminPermissions });
|
||||||
}, [adminPermissions, dispatch]);
|
}, [adminPermissions, dispatch]);
|
||||||
|
|
||||||
useEffect(() => {
|
const [
|
||||||
const currentToken = auth.getToken();
|
{ data: token, error: errorRenewToken },
|
||||||
|
{ data: initData, isLoading: isLoadingInit },
|
||||||
const renewToken = async () => {
|
{ data: telemetryProperties },
|
||||||
try {
|
] = useQueries([
|
||||||
|
{
|
||||||
|
queryKey: 'renew-token',
|
||||||
|
async queryFn() {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: {
|
||||||
data: { token },
|
data: { token },
|
||||||
},
|
},
|
||||||
} = await post('/admin/renew-token', { token: currentToken });
|
} = await post('/admin/renew-token', { token: auth.getToken() });
|
||||||
auth.updateToken(token);
|
|
||||||
} catch (err) {
|
|
||||||
// Refresh app
|
|
||||||
auth.clearAppStorage();
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (currentToken) {
|
return token;
|
||||||
renewToken();
|
},
|
||||||
}
|
|
||||||
}, [post]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
enabled: !!auth.getToken(),
|
||||||
const getData = async () => {
|
},
|
||||||
try {
|
|
||||||
|
{
|
||||||
|
queryKey: 'init',
|
||||||
|
async queryFn() {
|
||||||
const {
|
const {
|
||||||
data: {
|
data: { data },
|
||||||
data: { hasAdmin, uuid, menuLogo, authLogo },
|
|
||||||
},
|
|
||||||
} = await get(`/admin/init`);
|
} = await get(`/admin/init`);
|
||||||
|
|
||||||
updateProjectSettings({
|
return data;
|
||||||
menuLogo: prefixFileUrlWithBackendUrl(menuLogo),
|
},
|
||||||
authLogo: prefixFileUrlWithBackendUrl(authLogo),
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
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) {
|
return data;
|
||||||
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,
|
|
||||||
});
|
|
||||||
|
|
||||||
setTelemetryProperties(properties);
|
enabled: !!auth.getToken(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
try {
|
React.useEffect(() => {
|
||||||
const event = 'didInitializeAdministration';
|
// If the renew token could not be fetched, logout the user
|
||||||
await post(
|
if (errorRenewToken) {
|
||||||
'https://analytics.strapi.io/api/v2/track',
|
auth.clearAppStorage();
|
||||||
{
|
window.location.reload();
|
||||||
// This event is anonymous
|
} else if (token) {
|
||||||
event,
|
auth.updateToken(token);
|
||||||
userId: '',
|
}
|
||||||
deviceId,
|
}, [errorRenewToken, token]);
|
||||||
eventPropeties: {},
|
|
||||||
userProperties: { environment: appInfo.currentEnvironment },
|
|
||||||
groupProperties: { ...properties, projectId: uuid },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'X-Strapi-Event': event,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
// Silent.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setState({ isLoading: false, hasAdmin, uuid, deviceId });
|
// Store the fetched project settings (e.g. logos)
|
||||||
} catch (err) {
|
// TODO: this should be moved to redux
|
||||||
toggleNotification({
|
React.useEffect(() => {
|
||||||
type: 'warning',
|
if (!isLoadingInit && initData) {
|
||||||
message: { id: 'app.containers.App.notification.error.init' },
|
updateProjectSettings({
|
||||||
});
|
menuLogo: prefixFileUrlWithBackendUrl(initData.menuLogo),
|
||||||
}
|
authLogo: prefixFileUrlWithBackendUrl(initData.authLogo),
|
||||||
};
|
});
|
||||||
|
|
||||||
getData();
|
// TODO: this should be stored in redux
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
setState((prev) => ({
|
||||||
}, [toggleNotification, updateProjectSettings]);
|
...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,
|
uuid,
|
||||||
telemetryProperties,
|
telemetryProperties,
|
||||||
deviceId,
|
|
||||||
}),
|
}),
|
||||||
[uuid, telemetryProperties, deviceId]
|
[uuid, telemetryProperties]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoadingInit) {
|
||||||
return <LoadingIndicatorPage />;
|
return <LoadingIndicatorPage />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<LoadingIndicatorPage />}>
|
<React.Suspense fallback={<LoadingIndicatorPage />}>
|
||||||
<SkipToContent>{formatMessage({ id: 'skipToContent' })}</SkipToContent>
|
<SkipToContent>{formatMessage({ id: 'skipToContent' })}</SkipToContent>
|
||||||
<TrackingProvider value={trackingInfo}>
|
<TrackingProvider value={trackingContext}>
|
||||||
<Switch>
|
<Switch>
|
||||||
{authRoutes}
|
{authRoutes}
|
||||||
<Route
|
<Route
|
||||||
path="/auth/:authType"
|
path="/auth/:authType"
|
||||||
render={(routerProps) => (
|
render={(routerProps) => (
|
||||||
<AuthPage {...routerProps} setHasAdmin={setHasAdmin} hasAdmin={hasAdmin} />
|
<AuthPage
|
||||||
|
{...routerProps}
|
||||||
|
setHasAdmin={(hasAdmin) => {
|
||||||
|
// TODO: this should be a flag in redux
|
||||||
|
React.setState((prev) => ({ ...prev, hasAdmin }));
|
||||||
|
}}
|
||||||
|
hasAdmin={hasAdmin}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
exact
|
exact
|
||||||
/>
|
/>
|
||||||
@ -196,8 +238,6 @@ function App() {
|
|||||||
<Route path="" component={NotFoundPage} />
|
<Route path="" component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</TrackingProvider>
|
</TrackingProvider>
|
||||||
</Suspense>
|
</React.Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default App;
|
|
||||||
|
@ -18,7 +18,7 @@ import { FORMS } from './constants';
|
|||||||
import init from './init';
|
import init from './init';
|
||||||
import { initialState, reducer } from './reducer';
|
import { initialState, reducer } from './reducer';
|
||||||
|
|
||||||
const AuthPage = ({ hasAdmin, setHasAdmin }) => {
|
export const AuthPage = ({ hasAdmin, setHasAdmin }) => {
|
||||||
const {
|
const {
|
||||||
push,
|
push,
|
||||||
location: { search },
|
location: { search },
|
||||||
@ -315,5 +315,3 @@ AuthPage.propTypes = {
|
|||||||
hasAdmin: PropTypes.bool,
|
hasAdmin: PropTypes.bool,
|
||||||
setHasAdmin: PropTypes.func.isRequired,
|
setHasAdmin: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default AuthPage;
|
|
||||||
|
@ -11,7 +11,7 @@ import { LinkButton, useFocusWhenNavigate } from '@strapi/helper-plugin';
|
|||||||
import { ArrowRight, EmptyPictures } from '@strapi/icons';
|
import { ArrowRight, EmptyPictures } from '@strapi/icons';
|
||||||
import { useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
|
|
||||||
const NoContentType = () => {
|
export const NotFoundPage = () => {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
useFocusWhenNavigate();
|
useFocusWhenNavigate();
|
||||||
|
|
||||||
@ -46,5 +46,3 @@ const NoContentType = () => {
|
|||||||
</Main>
|
</Main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NoContentType;
|
|
||||||
|
@ -6,7 +6,7 @@ import { createMemoryHistory } from 'history';
|
|||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
|
|
||||||
import NotFoundPage from '../index';
|
import { NotFoundPage } from '../index';
|
||||||
|
|
||||||
const history = createMemoryHistory();
|
const history = createMemoryHistory();
|
||||||
|
|
||||||
|
@ -8,8 +8,8 @@ import { Redirect, Route, Switch, useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { useSettingsMenu } from '../../hooks';
|
import { useSettingsMenu } from '../../hooks';
|
||||||
import { useEnterprise } from '../../hooks/useEnterprise';
|
import { useEnterprise } from '../../hooks/useEnterprise';
|
||||||
import createRoute from '../../utils/createRoute';
|
// eslint-disable-next-line
|
||||||
import makeUniqueRoutes from '../../utils/makeUniqueRoutes';
|
import { createRoute, makeUniqueRoutes } from '../../utils';
|
||||||
|
|
||||||
import SettingsNav from './components/SettingsNav';
|
import SettingsNav from './components/SettingsNav';
|
||||||
import { ROUTES_CE } from './constants';
|
import { ROUTES_CE } from './constants';
|
||||||
|
@ -69,7 +69,7 @@ const TypographyCenter = styled(Typography)`
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const UseCasePage = () => {
|
export const UseCasePage = () => {
|
||||||
const toggleNotification = useNotification();
|
const toggleNotification = useNotification();
|
||||||
const { push, location } = useHistory();
|
const { push, location } = useHistory();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
@ -171,5 +171,3 @@ const UseCasePage = () => {
|
|||||||
</UnauthenticatedLayout>
|
</UnauthenticatedLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default UseCasePage;
|
|
||||||
|
@ -7,7 +7,7 @@ import { createMemoryHistory } from 'history';
|
|||||||
import { IntlProvider } from 'react-intl';
|
import { IntlProvider } from 'react-intl';
|
||||||
import { Router } from 'react-router-dom';
|
import { Router } from 'react-router-dom';
|
||||||
|
|
||||||
import UseCasePage from '../index';
|
import { UseCasePage } from '../index';
|
||||||
|
|
||||||
jest.mock('../../../components/LocalesProvider/useLocalesProvider', () => () => ({
|
jest.mock('../../../components/LocalesProvider/useLocalesProvider', () => () => ({
|
||||||
changeLocale() {},
|
changeLocale() {},
|
||||||
|
@ -4,6 +4,5 @@ export { default as formatAPIErrors } from './formatAPIErrors';
|
|||||||
export { default as getAttributesToDisplay } from './getAttributesToDisplay';
|
export { default as getAttributesToDisplay } from './getAttributesToDisplay';
|
||||||
export { default as getExistingActions } from './getExistingActions';
|
export { default as getExistingActions } from './getExistingActions';
|
||||||
export { default as getFullName } from './getFullName';
|
export { default as getFullName } from './getFullName';
|
||||||
export { default as makeUniqueRoutes } from './makeUniqueRoutes';
|
|
||||||
export { default as sortLinks } from './sortLinks';
|
export { default as sortLinks } from './sortLinks';
|
||||||
export { default as hashAdminUserEmail } from './uniqueAdminHash';
|
export { default as hashAdminUserEmail } from './uniqueAdminHash';
|
||||||
|
@ -1,6 +0,0 @@
|
|||||||
const makeUniqueRoutes = (routes) =>
|
|
||||||
routes.filter((route, index, refArray) => {
|
|
||||||
return refArray.findIndex((obj) => obj.key === route.key) === index;
|
|
||||||
});
|
|
||||||
|
|
||||||
export default makeUniqueRoutes;
|
|
Loading…
x
Reference in New Issue
Block a user