Merge branch 'main' into next

This commit is contained in:
Daniel Lautzenheiser
2023-02-27 13:19:11 -05:00
21 changed files with 740 additions and 817 deletions

View File

@ -4,15 +4,23 @@ import { styled } from '@mui/material/styles';
import React, { useCallback, useEffect, useMemo } from 'react';
import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
import { Navigate, Route, Routes, useLocation, useParams } from 'react-router-dom';
import {
Navigate,
Route,
Routes,
useLocation,
useNavigate,
useParams,
useSearchParams,
} from 'react-router-dom';
import { ScrollSync } from 'react-scroll-sync';
import TopBarProgress from 'react-topbar-progress-indicator';
import { loginUser as loginUserAction } from '@staticcms/core/actions/auth';
import { discardDraft as discardDraftAction } from '@staticcms/core/actions/entries';
import { discardDraft } 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';
import { useAppDispatch } from '@staticcms/core/store/hooks';
import { getDefaultPath } from '../../lib/util/collection.util';
import CollectionRoute from '../collection/CollectionRoute';
import EditorRoute from '../editor/EditorRoute';
@ -82,8 +90,10 @@ const App = ({
useMediaLibrary,
t,
scrollSyncEnabled,
discardDraft,
}: TranslatedProps<AppProps>) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const configError = useCallback(
(error?: string) => {
return (
@ -140,22 +150,28 @@ const App = ({
base_url={config.config.backend.base_url}
authEndpoint={config.config.backend.auth_endpoint}
config={config.config}
clearHash={() => history.replace('/')}
clearHash={() => navigate('/', { replace: true })}
t={t}
/>
</div>
);
}, [AuthComponent, auth.error, auth.isFetching, config.config, handleLogin, t]);
}, [AuthComponent, auth.error, auth.isFetching, config.config, handleLogin, navigate, t]);
const defaultPath = useMemo(() => getDefaultPath(collections), [collections]);
const { pathname } = useLocation();
const [searchParams] = useSearchParams();
useEffect(() => {
if (!/\/collections\/[a-zA-Z0-9_-]+\/entries\/[a-zA-Z0-9_-]+/g.test(pathname)) {
discardDraft();
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;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pathname]);
dispatch(discardDraft());
}, [dispatch, pathname, searchParams]);
const content = useMemo(() => {
if (!user) {
@ -265,7 +281,6 @@ function mapStateToProps(state: RootState) {
const mapDispatchToProps = {
loginUser: loginUserAction,
discardDraft: discardDraftAction,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@ -12,13 +12,13 @@ import Toolbar from '@mui/material/Toolbar';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { logoutUser as logoutUserAction } from '@staticcms/core/actions/auth';
import { createNewEntry } from '@staticcms/core/actions/collections';
import { openMediaLibrary as openMediaLibraryAction } from '@staticcms/core/actions/mediaLibrary';
import { checkBackendStatus as checkBackendStatusAction } from '@staticcms/core/actions/status';
import { buttons, colors } from '@staticcms/core/components/UI/styles';
import { stripProtocol } from '@staticcms/core/lib/urlHelper';
import { stripProtocol, getNewEntryUrl } from '@staticcms/core/lib/urlHelper';
import NavLink from '../UI/NavLink';
import SettingsDropdown from '../UI/SettingsDropdown';
@ -82,9 +82,7 @@ const Header = ({
setAnchorEl(null);
}, []);
const handleCreatePostClick = useCallback((collectionName: string) => {
createNewEntry(collectionName);
}, []);
const navigate = useNavigate();
const creatableCollections = useMemo(
() =>
@ -148,7 +146,7 @@ const Header = ({
{creatableCollections.map(collection => (
<MenuItem
key={collection.name}
onClick={() => handleCreatePostClick(collection.name)}
onClick={() => navigate(getNewEntryUrl(collection.name))}
>
{collection.label_singular || collection.label}
</MenuItem>

View File

@ -83,6 +83,10 @@ const CollectionView = ({
}, [collection]);
const newEntryUrl = useMemo(() => {
if (!collectionName || !collection) {
return undefined;
}
let url = 'fields' in collection && collection.create ? getNewEntryUrl(collectionName) : '';
if (url && filterTerm) {
url = getNewEntryUrl(collectionName);
@ -90,6 +94,7 @@ const CollectionView = ({
url = `${newEntryUrl}?path=${filterTerm}`;
}
}
return url;
}, [collection, collectionName, filterTerm]);
@ -120,6 +125,10 @@ const CollectionView = ({
);
}
if (!collection) {
return null;
}
return (
<EntriesCollection
collection={collection}
@ -142,21 +151,21 @@ const CollectionView = ({
const onSortClick = useCallback(
async (key: string, direction?: SortDirection) => {
await sortByField(collection, key, direction);
collection && (await sortByField(collection, key, direction));
},
[collection, sortByField],
);
const onFilterClick = useCallback(
async (filter: ViewFilter) => {
await filterByField(collection, filter);
collection && (await filterByField(collection, filter));
},
[collection, filterByField],
);
const onGroupClick = useCallback(
async (group: ViewGroup) => {
await groupByField(collection, group);
collection && (await groupByField(collection, group));
},
[collection, groupByField],
);
@ -176,7 +185,7 @@ const CollectionView = ({
return;
}
const defaultSort = collection.sortable_fields?.default;
const defaultSort = collection?.sortable_fields?.default;
if (!defaultSort || !defaultSort.field) {
if (!readyToLoad) {
setReadyToLoad(true);
@ -220,7 +229,7 @@ const CollectionView = ({
<>
<SearchResultContainer>
<SearchResultHeading>
{t(searchResultKey, { searchTerm, collection: collection.label })}
{t(searchResultKey, { searchTerm, collection: collection?.label })}
</SearchResultHeading>
</SearchResultContainer>
<CollectionControls viewStyle={viewStyle} onChangeViewStyle={changeViewStyle} t={t} />
@ -254,7 +263,7 @@ const CollectionView = ({
interface CollectionViewOwnProps {
isSearchResults?: boolean;
isSingleSearchResult?: boolean;
name: string;
name?: string;
searchTerm?: string;
filterTerm?: string;
}
@ -270,13 +279,13 @@ function mapStateToProps(state: RootState, ownProps: TranslatedProps<CollectionV
filterTerm = '',
t,
} = ownProps;
const collection: Collection = name ? collections[name] : collections[0];
const sort = selectEntriesSort(state, collection.name);
const collection = (name ? collections[name] : collections[0]) as Collection | undefined;
const sort = selectEntriesSort(state, collection?.name);
const sortableFields = selectSortableFields(collection, t);
const viewFilters = selectViewFilters(collection);
const viewGroups = selectViewGroups(collection);
const filter = selectEntriesFilter(state, collection.name);
const group = selectEntriesGroup(state, collection.name);
const filter = selectEntriesFilter(state, collection?.name);
const group = selectEntriesGroup(state, collection?.name);
const viewStyle = selectViewStyle(state);
return {

View File

@ -28,11 +28,11 @@ const CollectionRoute = ({
const defaultPath = useMemo(() => getDefaultPath(collections), [collections]);
if (!name || !collection) {
if (!searchTerm && (!name || !collection)) {
return <Navigate to={defaultPath} />;
}
if ('files' in collection && collection.files?.length === 1) {
if (collection && 'files' in collection && collection.files?.length === 1) {
return <Navigate to={`/collections/${collection.name}/entries/${collection.files[0].name}`} />;
}

View File

@ -25,6 +25,7 @@ const EntriesSearch = ({
searchEntries,
clearSearch,
}: EntriesSearchProps) => {
console.log('collections', collections);
const collectionNames = useMemo(() => Object.keys(collections), [collections]);
const getCursor = useCallback(() => {
@ -80,6 +81,7 @@ function mapStateToProps(state: RootState, ownProps: EntriesSearchOwnProps) {
const isFetching = state.search.isFetching;
const page = state.search.page;
const entries = selectSearchedEntries(state, collectionNames);
console.log('searched entries', entries);
return { isFetching, page, collections, viewStyle, entries, searchTerm };
}

View File

@ -10,8 +10,8 @@ import ListItemText from '@mui/material/ListItemText';
import Typography from '@mui/material/Typography';
import React, { useMemo } from 'react';
import { translate } from 'react-polyglot';
import { useNavigate } from 'react-router-dom';
import { searchCollections } from '@staticcms/core/actions/collections';
import { colors } from '@staticcms/core/components/UI/styles';
import { getAdditionalLinks, getIcon } from '@staticcms/core/lib/registry';
import NavLink from '../UI/NavLink';
@ -48,6 +48,15 @@ const Sidebar = ({
t,
filterTerm,
}: TranslatedProps<SidebarProps>) => {
const navigate = useNavigate();
function searchCollections(query: string, collection?: string) {
if (collection) {
navigate(`/collections/${collection}/search/${query}`);
} else {
navigate(`/search/${query}`);
}
}
const collectionLinks = useMemo(
() =>
Object.values(collections)

View File

@ -1,31 +1,28 @@
import { createHashHistory } from 'history';
import debounce from 'lodash/debounce';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { logoutUser as logoutUserAction } from '@staticcms/core/actions/auth';
import { logoutUser } from '@staticcms/core/actions/auth';
import {
createDraftDuplicateFromEntry as createDraftDuplicateFromEntryAction,
createEmptyDraft as createEmptyDraftAction,
deleteDraftLocalBackup as deleteDraftLocalBackupAction,
deleteEntry as deleteEntryAction,
deleteLocalBackup as deleteLocalBackupAction,
discardDraft as discardDraftAction,
loadEntries as loadEntriesAction,
loadEntry as loadEntryAction,
loadLocalBackup as loadLocalBackupAction,
persistEntry as persistEntryAction,
persistLocalBackup as persistLocalBackupAction,
retrieveLocalBackup as retrieveLocalBackupAction,
createDraftDuplicateFromEntry,
createEmptyDraft,
deleteDraftLocalBackup,
deleteEntry,
deleteLocalBackup,
loadEntry,
loadLocalBackup,
persistEntry,
persistLocalBackup,
retrieveLocalBackup,
} from '@staticcms/core/actions/entries';
import {
loadScroll as loadScrollAction,
toggleScroll as toggleScrollAction,
} from '@staticcms/core/actions/scroll';
import { loadScroll, toggleScroll } from '@staticcms/core/actions/scroll';
import { selectFields } from '@staticcms/core/lib/util/collection.util';
import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
import { selectEntry } from '@staticcms/core/reducers/selectors/entries';
import { history, navigateToCollection, navigateToNewEntry } from '@staticcms/core/routing/history';
import { useAppDispatch } from '@staticcms/core/store/hooks';
import confirm from '../UI/Confirm';
import Loader from '../UI/Loader';
import EditorInterface from './EditorInterface';
@ -38,10 +35,10 @@ import type {
} from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { Blocker } from 'history';
import type { ComponentType } from 'react';
import type { ComponentType, FC } from 'react';
import type { ConnectedProps } from 'react-redux';
const Editor = ({
const Editor: FC<TranslatedProps<EditorProps>> = ({
entry,
entryDraft,
fields,
@ -50,34 +47,25 @@ const Editor = ({
hasChanged,
displayUrl,
isModification,
logoutUser,
draftKey,
t,
editorBackLink,
toggleScroll,
scrollSyncEnabled,
loadScroll,
showDelete,
slug,
localBackup,
persistLocalBackup,
loadEntry,
persistEntry,
deleteEntry,
loadLocalBackup,
retrieveLocalBackup,
deleteLocalBackup,
deleteDraftLocalBackup,
createDraftDuplicateFromEntry,
createEmptyDraft,
discardDraft,
}: TranslatedProps<EditorProps>) => {
t,
}) => {
const [version, setVersion] = useState(0);
const history = createHashHistory();
const dispatch = useAppDispatch();
const navigate = useNavigate();
const createBackup = useMemo(
() =>
debounce(function (entry: Entry, collection: Collection) {
persistLocalBackup(entry, collection);
dispatch(persistLocalBackup(entry, collection));
}, 2000),
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
@ -86,9 +74,9 @@ const Editor = ({
const deleteBackup = useCallback(() => {
createBackup.cancel();
if (slug) {
deleteLocalBackup(collection, slug);
dispatch(deleteLocalBackup(collection, slug));
}
deleteDraftLocalBackup();
dispatch(deleteDraftLocalBackup());
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collection, createBackup, slug]);
@ -105,29 +93,29 @@ const Editor = ({
setTimeout(async () => {
try {
await persistEntry(collection);
await dispatch(persistEntry(collection, navigate));
setVersion(version + 1);
deleteBackup();
if (createNew) {
navigateToNewEntry(collection.name);
if (duplicate && entryDraft.entry) {
createDraftDuplicateFromEntry(entryDraft.entry);
dispatch(createDraftDuplicateFromEntry(entryDraft.entry));
navigate(`/collections/${collection.name}/new`, { replace: true });
} else {
setSubmitted(false);
setTimeout(() => {
dispatch(createEmptyDraft(collection, location.search));
setVersion(version + 1);
navigate(`/collections/${collection.name}/new`, { replace: true });
}, 100);
}
}
// eslint-disable-next-line no-empty
} catch (e) {}
}, 100);
},
[
collection,
createDraftDuplicateFromEntry,
deleteBackup,
entryDraft.entry,
persistEntry,
version,
],
[collection, deleteBackup, dispatch, entryDraft.entry, navigate, version],
);
const handleDuplicateEntry = useCallback(() => {
@ -135,9 +123,9 @@ const Editor = ({
return;
}
navigateToNewEntry(collection.name);
createDraftDuplicateFromEntry(entryDraft.entry);
}, [collection.name, createDraftDuplicateFromEntry, entryDraft.entry]);
dispatch(createDraftDuplicateFromEntry(entryDraft.entry));
navigate(`/collections/${collection.name}/new?duplicate=true`, { replace: true });
}, [collection.name, dispatch, entryDraft.entry, navigate]);
const handleDeleteEntry = useCallback(async () => {
if (entryDraft.hasChanged) {
@ -161,15 +149,15 @@ const Editor = ({
}
if (!slug) {
return navigateToCollection(collection.name);
return navigate(`/collections/${collection.name}`);
}
setTimeout(async () => {
await deleteEntry(collection, slug);
await dispatch(deleteEntry(collection, slug));
deleteBackup();
return navigateToCollection(collection.name);
return navigate(`/collections/${collection.name}`);
}, 0);
}, [collection, deleteBackup, deleteEntry, entryDraft.hasChanged, slug]);
}, [collection, deleteBackup, dispatch, entryDraft.hasChanged, navigate, slug]);
const [prevLocalBackup, setPrevLocalBackup] = useState<
| {
@ -187,7 +175,7 @@ const Editor = ({
});
if (confirmLoadBackupBody) {
loadLocalBackup();
dispatch(loadLocalBackup());
setVersion(version + 1);
} else {
deleteBackup();
@ -198,7 +186,7 @@ const Editor = ({
}
setPrevLocalBackup(localBackup);
}, [deleteBackup, loadLocalBackup, localBackup, prevLocalBackup, version]);
}, [deleteBackup, dispatch, localBackup, prevLocalBackup, version]);
useEffect(() => {
if (hasChanged && entryDraft.entry) {
@ -211,32 +199,22 @@ const Editor = ({
}, [collection, createBackup, entryDraft.entry, hasChanged]);
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
const [preSlug, setPrevSlug] = useState<string | undefined | null>(null);
const [prevSlug, setPrevSlug] = useState<string | undefined | null>(null);
useEffect(() => {
if (!slug && preSlug !== slug) {
if (!slug && prevSlug !== slug) {
setTimeout(() => {
createEmptyDraft(collection, location.search);
dispatch(createEmptyDraft(collection, location.search));
});
} else if (slug && (prevCollection !== collection || preSlug !== slug)) {
} else if (slug && (prevCollection !== collection || prevSlug !== slug)) {
setTimeout(() => {
retrieveLocalBackup(collection, slug);
loadEntry(collection, slug);
dispatch(retrieveLocalBackup(collection, slug));
dispatch(loadEntry(collection, slug));
});
}
setPrevCollection(collection);
setPrevSlug(slug);
}, [
collection,
createEmptyDraft,
discardDraft,
entryDraft.entry,
loadEntry,
preSlug,
prevCollection,
retrieveLocalBackup,
slug,
]);
}, [collection, entryDraft.entry, prevSlug, prevCollection, slug, dispatch]);
const leaveMessage = useMemo(() => t('editor.editor.onLeavePage'), [t]);
@ -284,7 +262,19 @@ const Editor = ({
return () => {
unblock();
};
}, [collection.name, deleteBackup, discardDraft, navigationBlocker]);
}, [collection.name, history, navigationBlocker]);
const handleLogout = useCallback(() => {
dispatch(logoutUser());
}, [dispatch]);
const handleToggleScroll = useCallback(async () => {
await dispatch(toggleScroll());
}, [dispatch]);
const handleLoadScroll = useCallback(async () => {
await dispatch(loadScroll());
}, [dispatch]);
if (entry && entry.error) {
return (
@ -313,11 +303,11 @@ const Editor = ({
displayUrl={displayUrl}
isNewEntry={!slug}
isModification={isModification}
onLogoutClick={logoutUser}
onLogoutClick={handleLogout}
editorBackLink={editorBackLink}
toggleScroll={toggleScroll}
toggleScroll={handleToggleScroll}
scrollSyncEnabled={scrollSyncEnabled}
loadScroll={loadScroll}
loadScroll={handleLoadScroll}
submitted={submitted}
t={t}
/>
@ -378,25 +368,7 @@ function mapStateToProps(state: RootState, ownProps: CollectionViewOwnProps) {
};
}
const mapDispatchToProps = {
loadEntry: loadEntryAction,
loadEntries: loadEntriesAction,
loadLocalBackup: loadLocalBackupAction,
deleteDraftLocalBackup: deleteDraftLocalBackupAction,
retrieveLocalBackup: retrieveLocalBackupAction,
persistLocalBackup: persistLocalBackupAction,
deleteLocalBackup: deleteLocalBackupAction,
createDraftDuplicateFromEntry: createDraftDuplicateFromEntryAction,
createEmptyDraft: createEmptyDraftAction,
discardDraft: discardDraftAction,
persistEntry: persistEntryAction,
deleteEntry: deleteEntryAction,
logoutUser: logoutUserAction,
toggleScroll: toggleScrollAction,
loadScroll: loadScrollAction,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
const connector = connect(mapStateToProps);
export type EditorProps = ConnectedProps<typeof connector>;
export default connector(translate()(Editor) as ComponentType<EditorProps>);

View File

@ -137,7 +137,7 @@ interface EditorInterfaceProps {
isModification: boolean;
onLogoutClick: () => void;
editorBackLink: string;
toggleScroll: () => Promise<{ readonly type: 'TOGGLE_SCROLL' }>;
toggleScroll: () => Promise<void>;
scrollSyncEnabled: boolean;
loadScroll: () => void;
submitted: boolean;

View File

@ -1,6 +1,6 @@
import { styled } from '@mui/material/styles';
import React, { useCallback, useMemo } from 'react';
import ReactDOM from 'react-dom';
import { createPortal } from 'react-dom';
import Frame from 'react-frame-component';
import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
@ -132,7 +132,7 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
return null;
}
return ReactDOM.createPortal(
return createPortal(
<StyledPreviewContent className="preview-content">
{!entry || !entry.data ? null : (
<ErrorBoundary config={config}>