2022-10-20 11:57:30 -04:00
|
|
|
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
|
|
|
import Fab from '@mui/material/Fab';
|
2022-10-26 10:57:40 -04:00
|
|
|
import { styled } from '@mui/material/styles';
|
2022-12-01 19:29:33 -05:00
|
|
|
import React, { useCallback, useEffect, useMemo } from 'react';
|
2022-10-20 11:57:30 -04:00
|
|
|
import { translate } from 'react-polyglot';
|
|
|
|
import { connect } from 'react-redux';
|
2022-11-02 08:54:30 -04:00
|
|
|
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
|
2022-10-20 11:57:30 -04:00
|
|
|
import { ScrollSync } from 'react-scroll-sync';
|
|
|
|
import TopBarProgress from 'react-topbar-progress-indicator';
|
|
|
|
|
2022-12-01 19:29:33 -05:00
|
|
|
import { loginUser as loginUserAction } from '@staticcms/core/actions/auth';
|
|
|
|
import { discardDraft as discardDraftAction } from '@staticcms/core/actions/entries';
|
|
|
|
import { currentBackend } from '@staticcms/core/backend';
|
|
|
|
import { colors, GlobalStyles } from '@staticcms/core/components/UI/styles';
|
|
|
|
import { history } from '@staticcms/core/routing/history';
|
2022-10-20 11:57:30 -04:00
|
|
|
import CollectionRoute from '../Collection/CollectionRoute';
|
|
|
|
import EditorRoute from '../Editor/EditorRoute';
|
|
|
|
import MediaLibrary from '../MediaLibrary/MediaLibrary';
|
2022-11-04 17:41:12 -04:00
|
|
|
import Page from '../page/Page';
|
2022-10-20 11:57:30 -04:00
|
|
|
import Snackbars from '../snackbar/Snackbars';
|
|
|
|
import { Alert } from '../UI/Alert';
|
|
|
|
import { Confirm } from '../UI/Confirm';
|
|
|
|
import Loader from '../UI/Loader';
|
|
|
|
import ScrollTop from '../UI/ScrollTop';
|
|
|
|
import NotFoundPage from './NotFoundPage';
|
|
|
|
|
2022-12-01 19:29:33 -05:00
|
|
|
import type { Collections, Credentials, TranslatedProps } from '@staticcms/core/interface';
|
|
|
|
import type { RootState } from '@staticcms/core/store';
|
2022-10-20 11:57:30 -04:00
|
|
|
import type { ComponentType } from 'react';
|
|
|
|
import type { ConnectedProps } from 'react-redux';
|
2022-11-02 08:54:30 -04:00
|
|
|
|
2022-10-20 11:57:30 -04:00
|
|
|
TopBarProgress.config({
|
|
|
|
barColors: {
|
|
|
|
0: colors.active,
|
|
|
|
'1.0': colors.active,
|
|
|
|
},
|
|
|
|
shadowBlur: 0,
|
|
|
|
barThickness: 2,
|
|
|
|
});
|
|
|
|
|
|
|
|
const AppRoot = styled('div')`
|
|
|
|
width: 100%;
|
|
|
|
min-width: 1200px;
|
|
|
|
height: 100vh;
|
|
|
|
position: relative;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const AppWrapper = styled('div')`
|
|
|
|
width: 100%;
|
|
|
|
min-width: 1200px;
|
|
|
|
min-height: 100vh;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const ErrorContainer = styled('div')`
|
|
|
|
margin: 20px;
|
|
|
|
`;
|
|
|
|
|
|
|
|
const ErrorCodeBlock = styled('pre')`
|
|
|
|
margin-left: 20px;
|
|
|
|
font-size: 15px;
|
|
|
|
line-height: 1.5;
|
|
|
|
`;
|
|
|
|
|
|
|
|
function getDefaultPath(collections: Collections) {
|
|
|
|
const options = Object.values(collections).filter(
|
|
|
|
collection =>
|
|
|
|
collection.hide !== true && (!('files' in collection) || (collection.files?.length ?? 0) > 1),
|
|
|
|
);
|
|
|
|
|
|
|
|
if (options.length > 0) {
|
|
|
|
return `/collections/${options[0].name}`;
|
|
|
|
} else {
|
|
|
|
throw new Error('Could not find a non hidden collection');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function CollectionSearchRedirect() {
|
|
|
|
const { name } = useParams();
|
|
|
|
return <Navigate to={`/collections/${name}`} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
function EditEntityRedirect() {
|
|
|
|
const { name, entryName } = useParams();
|
|
|
|
return <Navigate to={`/collections/${name}/entries/${entryName}`} />;
|
|
|
|
}
|
|
|
|
|
|
|
|
const App = ({
|
|
|
|
auth,
|
|
|
|
user,
|
|
|
|
config,
|
|
|
|
collections,
|
|
|
|
loginUser,
|
|
|
|
isFetching,
|
|
|
|
useMediaLibrary,
|
|
|
|
t,
|
|
|
|
scrollSyncEnabled,
|
2022-11-02 08:54:30 -04:00
|
|
|
discardDraft,
|
2022-10-20 11:57:30 -04:00
|
|
|
}: TranslatedProps<AppProps>) => {
|
2022-10-26 10:57:40 -04:00
|
|
|
const configError = useCallback(
|
|
|
|
(error?: string) => {
|
|
|
|
return (
|
|
|
|
<ErrorContainer>
|
|
|
|
<h1>{t('app.app.errorHeader')}</h1>
|
|
|
|
<div>
|
|
|
|
<strong>{t('app.app.configErrors')}:</strong>
|
|
|
|
<ErrorCodeBlock>{error ?? config.error}</ErrorCodeBlock>
|
|
|
|
<span>{t('app.app.checkConfigYml')}</span>
|
|
|
|
</div>
|
|
|
|
</ErrorContainer>
|
|
|
|
);
|
|
|
|
},
|
|
|
|
[config.error, t],
|
|
|
|
);
|
2022-10-20 11:57:30 -04:00
|
|
|
|
|
|
|
const handleLogin = useCallback(
|
|
|
|
(credentials: Credentials) => {
|
|
|
|
loginUser(credentials);
|
|
|
|
},
|
|
|
|
[loginUser],
|
|
|
|
);
|
|
|
|
|
2022-10-26 09:23:11 -04:00
|
|
|
const AuthComponent = useMemo(() => {
|
2022-10-20 11:57:30 -04:00
|
|
|
if (!config.config) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const backend = currentBackend(config.config);
|
2022-10-26 09:23:11 -04:00
|
|
|
return backend?.authComponent();
|
|
|
|
}, [config.config]);
|
|
|
|
|
|
|
|
const authenticationPage = useMemo(() => {
|
|
|
|
if (!config.config) {
|
|
|
|
return null;
|
|
|
|
}
|
2022-10-20 11:57:30 -04:00
|
|
|
|
2022-10-26 09:23:11 -04:00
|
|
|
if (AuthComponent == null) {
|
2022-10-20 11:57:30 -04:00
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<h1>{t('app.app.waitingBackend')}</h1>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
2022-10-26 09:23:11 -04:00
|
|
|
<div key="auth-page-wrapper">
|
|
|
|
<AuthComponent
|
|
|
|
key="auth-page"
|
|
|
|
onLogin={handleLogin}
|
|
|
|
error={auth.error}
|
|
|
|
inProgress={auth.isFetching}
|
|
|
|
siteId={config.config.backend.site_domain}
|
|
|
|
base_url={config.config.backend.base_url}
|
|
|
|
authEndpoint={config.config.backend.auth_endpoint}
|
|
|
|
config={config.config}
|
|
|
|
clearHash={() => history.replace('/')}
|
|
|
|
t={t}
|
|
|
|
/>
|
2022-10-20 11:57:30 -04:00
|
|
|
</div>
|
|
|
|
);
|
2022-10-26 09:23:11 -04:00
|
|
|
}, [AuthComponent, auth.error, auth.isFetching, config.config, handleLogin, t]);
|
2022-10-20 11:57:30 -04:00
|
|
|
|
|
|
|
const defaultPath = useMemo(() => getDefaultPath(collections), [collections]);
|
|
|
|
|
2022-11-02 08:54:30 -04:00
|
|
|
const { pathname } = useLocation();
|
2022-12-01 19:29:33 -05:00
|
|
|
useEffect(() => {
|
2022-11-02 08:54:30 -04:00
|
|
|
if (!/\/collections\/[a-zA-Z0-9_-]+\/entries\/[a-zA-Z0-9_-]+/g.test(pathname)) {
|
|
|
|
discardDraft();
|
|
|
|
}
|
|
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
|
}, [pathname]);
|
|
|
|
|
2022-10-26 10:57:40 -04:00
|
|
|
const content = useMemo(() => {
|
|
|
|
if (!user) {
|
|
|
|
return authenticationPage;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
{isFetching && <TopBarProgress />}
|
|
|
|
<Routes>
|
|
|
|
<Route path="/" element={<Navigate to={defaultPath} />} />
|
|
|
|
<Route path="/search" element={<Navigate to={defaultPath} />} />
|
|
|
|
<Route path="/collections/:name/search/" element={<CollectionSearchRedirect />} />
|
|
|
|
<Route
|
|
|
|
path="/error=access_denied&error_description=Signups+not+allowed+for+this+instance"
|
|
|
|
element={<Navigate to={defaultPath} />}
|
|
|
|
/>
|
|
|
|
<Route path="/collections" element={<CollectionRoute collections={collections} />} />
|
|
|
|
<Route
|
|
|
|
path="/collections/:name"
|
|
|
|
element={<CollectionRoute collections={collections} />}
|
|
|
|
/>
|
|
|
|
<Route
|
|
|
|
path="/collections/:name/new"
|
|
|
|
element={<EditorRoute collections={collections} newRecord />}
|
|
|
|
/>
|
|
|
|
<Route
|
|
|
|
path="/collections/:name/entries/:slug"
|
|
|
|
element={<EditorRoute collections={collections} />}
|
|
|
|
/>
|
|
|
|
<Route
|
|
|
|
path="/collections/:name/search/:searchTerm"
|
|
|
|
element={
|
|
|
|
<CollectionRoute collections={collections} isSearchResults isSingleSearchResult />
|
|
|
|
}
|
|
|
|
/>
|
|
|
|
<Route
|
|
|
|
path="/collections/:name/filter/:filterTerm"
|
|
|
|
element={<CollectionRoute collections={collections} />}
|
|
|
|
/>
|
|
|
|
<Route
|
|
|
|
path="/search/:searchTerm"
|
|
|
|
element={<CollectionRoute collections={collections} isSearchResults />}
|
|
|
|
/>
|
|
|
|
<Route path="/edit/:name/:entryName" element={<EditEntityRedirect />} />
|
2022-11-04 17:41:12 -04:00
|
|
|
<Route path="/page/:id" element={<Page />} />
|
2022-10-26 10:57:40 -04:00
|
|
|
<Route element={<NotFoundPage />} />
|
|
|
|
</Routes>
|
|
|
|
{useMediaLibrary ? <MediaLibrary /> : null}
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
}, [authenticationPage, collections, defaultPath, isFetching, useMediaLibrary, user]);
|
|
|
|
|
2022-10-20 11:57:30 -04:00
|
|
|
if (!config.config) {
|
2022-10-26 10:57:40 -04:00
|
|
|
return configError(t('app.app.configNotFound'));
|
2022-10-20 11:57:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
if (config.error) {
|
|
|
|
return configError();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (config.isFetching) {
|
|
|
|
return <Loader>{t('app.app.loadingConfig')}</Loader>;
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
2022-10-26 10:57:40 -04:00
|
|
|
<GlobalStyles key="global-styles" />
|
|
|
|
<ScrollSync key="scroll-sync" enabled={scrollSyncEnabled}>
|
2022-10-20 11:57:30 -04:00
|
|
|
<>
|
2022-10-26 10:57:40 -04:00
|
|
|
<div key="back-to-top-anchor" id="back-to-top-anchor" />
|
|
|
|
<AppRoot key="cms-root" id="cms-root">
|
|
|
|
<AppWrapper key="cms-wrapper" className="cms-wrapper">
|
|
|
|
<Snackbars key="snackbars" />
|
|
|
|
{content}
|
|
|
|
<Alert key="alert" />
|
|
|
|
<Confirm key="confirm" />
|
2022-10-20 11:57:30 -04:00
|
|
|
</AppWrapper>
|
|
|
|
</AppRoot>
|
2022-10-26 10:57:40 -04:00
|
|
|
<ScrollTop key="scroll-to-top">
|
2022-10-20 11:57:30 -04:00
|
|
|
<Fab size="small" aria-label="scroll back to top">
|
|
|
|
<KeyboardArrowUpIcon />
|
|
|
|
</Fab>
|
|
|
|
</ScrollTop>
|
|
|
|
</>
|
|
|
|
</ScrollSync>
|
|
|
|
</>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
function mapStateToProps(state: RootState) {
|
|
|
|
const { auth, config, collections, globalUI, mediaLibrary, scroll } = state;
|
|
|
|
const user = auth.user;
|
|
|
|
const isFetching = globalUI.isFetching;
|
|
|
|
const useMediaLibrary = !mediaLibrary.externalLibrary;
|
|
|
|
const scrollSyncEnabled = scroll.isScrolling;
|
|
|
|
return {
|
|
|
|
auth,
|
|
|
|
config,
|
|
|
|
collections,
|
|
|
|
user,
|
|
|
|
isFetching,
|
|
|
|
useMediaLibrary,
|
|
|
|
scrollSyncEnabled,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
const mapDispatchToProps = {
|
|
|
|
loginUser: loginUserAction,
|
2022-11-02 08:54:30 -04:00
|
|
|
discardDraft: discardDraftAction,
|
2022-10-20 11:57:30 -04:00
|
|
|
};
|
|
|
|
|
|
|
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
|
|
|
export type AppProps = ConnectedProps<typeof connector>;
|
|
|
|
|
|
|
|
export default connector(translate()(App) as ComponentType<AppProps>);
|