From eda812991297d6d515e225ad2837e6fc4ee52fe5 Mon Sep 17 00:00:00 2001 From: Daniel Lautzenheiser Date: Mon, 27 Feb 2023 09:12:20 -0500 Subject: [PATCH] fix: duplicate create new (#596) --- packages/core/src/actions/entries.ts | 6 +- packages/core/src/components/App/App.tsx | 32 +++- .../core/src/components/Editor/Editor.tsx | 161 +++++++----------- .../src/components/Editor/EditorInterface.tsx | 2 +- 4 files changed, 91 insertions(+), 110 deletions(-) diff --git a/packages/core/src/actions/entries.ts b/packages/core/src/actions/entries.ts index 512ba467..4683e751 100644 --- a/packages/core/src/actions/entries.ts +++ b/packages/core/src/actions/entries.ts @@ -1,5 +1,4 @@ import isEqual from 'lodash/isEqual'; -import { redirect } from 'react-router-dom'; import { currentBackend } from '../backend'; import { @@ -56,6 +55,7 @@ import { addAssets, getAsset } from './media'; import { loadMedia, waitForMediaLibraryToLoad } from './mediaLibrary'; import { waitUntil } from './waitUntil'; +import type { NavigateFunction } from 'react-router-dom'; import type { AnyAction } from 'redux'; import type { ThunkDispatch } from 'redux-thunk'; import type { Backend } from '../backend'; @@ -951,7 +951,7 @@ export function getSerializedEntry(collection: Collection, entry: Entry): Entry return serializedEntry; } -export function persistEntry(collection: Collection) { +export function persistEntry(collection: Collection, navigate: NavigateFunction) { return async (dispatch: ThunkDispatch, getState: () => RootState) => { const state = getState(); const entryDraft = state.entryDraft; @@ -1040,7 +1040,7 @@ export function persistEntry(collection: Collection) { } if (entry.slug !== newSlug) { await dispatch(loadEntry(collection, newSlug)); - redirect(`/collections/${collection.name}/entries/${newSlug}`); + navigate(`/collections/${collection.name}/entries/${newSlug}`); } else { await dispatch(loadEntry(collection, newSlug, true)); } diff --git a/packages/core/src/components/App/App.tsx b/packages/core/src/components/App/App.tsx index 3c0aa0d8..0f329e48 100644 --- a/packages/core/src/components/App/App.tsx +++ b/packages/core/src/components/App/App.tsx @@ -4,14 +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, useNavigate, 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 { useAppDispatch } from '@staticcms/core/store/hooks'; import { getDefaultPath } from '../../lib/util/collection.util'; import CollectionRoute from '../Collection/CollectionRoute'; import EditorRoute from '../Editor/EditorRoute'; @@ -81,9 +90,9 @@ const App = ({ useMediaLibrary, t, scrollSyncEnabled, - discardDraft, }: TranslatedProps) => { const navigate = useNavigate(); + const dispatch = useAppDispatch(); const configError = useCallback( (error?: string) => { @@ -146,17 +155,23 @@ const App = ({ /> ); - }, [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) { @@ -266,7 +281,6 @@ function mapStateToProps(state: RootState) { const mapDispatchToProps = { loginUser: loginUserAction, - discardDraft: discardDraftAction, }; const connector = connect(mapStateToProps, mapDispatchToProps); diff --git a/packages/core/src/components/Editor/Editor.tsx b/packages/core/src/components/Editor/Editor.tsx index 5885aca5..cbe33da9 100644 --- a/packages/core/src/components/Editor/Editor.tsx +++ b/packages/core/src/components/Editor/Editor.tsx @@ -1,32 +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 { createHashHistory } from 'history'; -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 { useAppDispatch } from '@staticcms/core/store/hooks'; import confirm from '../UI/Confirm'; import Loader from '../UI/Loader'; import EditorInterface from './EditorInterface'; @@ -39,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> = ({ entry, entryDraft, fields, @@ -51,38 +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) => { + 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 [], @@ -91,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]); @@ -110,29 +93,29 @@ const Editor = ({ setTimeout(async () => { try { - await persistEntry(collection); + await dispatch(persistEntry(collection, navigate)); setVersion(version + 1); deleteBackup(); if (createNew) { - navigate(`/collections/${collection.name}/new`, { replace: true }); 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(() => { @@ -140,9 +123,9 @@ const Editor = ({ return; } - navigate(`/collections/${collection.name}/new`, { replace: true }); - 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) { @@ -170,11 +153,11 @@ const Editor = ({ } setTimeout(async () => { - await deleteEntry(collection, slug); + await dispatch(deleteEntry(collection, slug)); deleteBackup(); return navigate(`/collections/${collection.name}`); }, 0); - }, [collection, deleteBackup, deleteEntry, entryDraft.hasChanged, slug]); + }, [collection, deleteBackup, dispatch, entryDraft.hasChanged, navigate, slug]); const [prevLocalBackup, setPrevLocalBackup] = useState< | { @@ -192,7 +175,7 @@ const Editor = ({ }); if (confirmLoadBackupBody) { - loadLocalBackup(); + dispatch(loadLocalBackup()); setVersion(version + 1); } else { deleteBackup(); @@ -203,7 +186,7 @@ const Editor = ({ } setPrevLocalBackup(localBackup); - }, [deleteBackup, loadLocalBackup, localBackup, prevLocalBackup, version]); + }, [deleteBackup, dispatch, localBackup, prevLocalBackup, version]); useEffect(() => { if (hasChanged && entryDraft.entry) { @@ -216,32 +199,22 @@ const Editor = ({ }, [collection, createBackup, entryDraft.entry, hasChanged]); const [prevCollection, setPrevCollection] = useState(null); - const [preSlug, setPrevSlug] = useState(null); + const [prevSlug, setPrevSlug] = useState(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]); @@ -289,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 ( @@ -318,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} /> @@ -383,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; export default connector(translate()(Editor) as ComponentType); diff --git a/packages/core/src/components/Editor/EditorInterface.tsx b/packages/core/src/components/Editor/EditorInterface.tsx index 94d9b830..c2488501 100644 --- a/packages/core/src/components/Editor/EditorInterface.tsx +++ b/packages/core/src/components/Editor/EditorInterface.tsx @@ -137,7 +137,7 @@ interface EditorInterfaceProps { isModification: boolean; onLogoutClick: () => void; editorBackLink: string; - toggleScroll: () => Promise<{ readonly type: 'TOGGLE_SCROLL' }>; + toggleScroll: () => Promise; scrollSyncEnabled: boolean; loadScroll: () => void; submitted: boolean;