290 lines
8.2 KiB
TypeScript
Raw Normal View History

2022-10-20 11:57:30 -04:00
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Fab from '@mui/material/Fab';
import { styled } from '@mui/material/styles';
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';
2023-02-27 09:12:20 -05:00
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
useParams,
useSearchParams,
} 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';
import { loginUser as loginUserAction } from '@staticcms/core/actions/auth';
2023-02-27 09:12:20 -05:00
import { discardDraft } from '@staticcms/core/actions/entries';
import { currentBackend } from '@staticcms/core/backend';
import { colors, GlobalStyles } from '@staticcms/core/components/UI/styles';
2023-02-27 09:12:20 -05:00
import { useAppDispatch } from '@staticcms/core/store/hooks';
import { getDefaultPath } from '../../lib/util/collection.util';
2023-02-16 13:34:35 -05:00
import CollectionRoute from '../collection/CollectionRoute';
import EditorRoute from '../editor/EditorRoute';
2022-10-20 11:57:30 -04:00
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';
import type { 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-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 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,
}: TranslatedProps<AppProps>) => {
2023-02-26 15:25:27 +01:00
const navigate = useNavigate();
2023-02-27 09:12:20 -05:00
const dispatch = useAppDispatch();
2023-02-26 15:25:27 +01: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}
2023-02-26 15:25:27 +01:00
clearHash={() => navigate('/', { replace: true })}
2022-10-26 09:23:11 -04:00
t={t}
/>
2022-10-20 11:57:30 -04:00
</div>
);
2023-02-27 09:12:20 -05:00
}, [AuthComponent, auth.error, auth.isFetching, config.config, handleLogin, navigate, t]);
2022-10-20 11:57:30 -04:00
const defaultPath = useMemo(() => getDefaultPath(collections), [collections]);
const { pathname } = useLocation();
2023-02-27 09:12:20 -05:00
const [searchParams] = useSearchParams();
useEffect(() => {
2023-02-27 09:12:20 -05:00
if (
/\/collections\/[a-zA-Z0-9_-]+\/entries\/[a-zA-Z0-9_-]+/g.test(pathname) ||
(/\/collections\/[a-zA-Z0-9_-]+\/new/g.test(pathname) &&
searchParams.get('duplicate') === 'true')
) {
return;
}
2023-02-27 09:12:20 -05:00
dispatch(discardDraft());
}, [dispatch, pathname, searchParams]);
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
2023-02-16 13:34:35 -05:00
path="/collections/:name/entries/*"
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 />} />
<Route element={<NotFoundPage />} />
</Routes>
{useMediaLibrary ? <MediaLibrary /> : null}
</>
);
}, [authenticationPage, collections, defaultPath, isFetching, useMediaLibrary, user]);
2022-10-20 11:57:30 -04:00
if (!config.config) {
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 (
<>
<GlobalStyles key="global-styles" />
<ScrollSync key="scroll-sync" enabled={scrollSyncEnabled}>
2022-10-20 11:57:30 -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>
<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,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export type AppProps = ConnectedProps<typeof connector>;
export default connector(translate()(App) as ComponentType<AppProps>);