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

@ -1,5 +1,5 @@
{ {
"$schema": "node_modules/lerna/schemas/lerna-schema.json", "$schema": "node_modules/lerna/schemas/lerna-schema.json",
"useWorkspaces": true, "useWorkspaces": true,
"version": "1.2.10" "version": "1.2.12"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "@staticcms/app", "name": "@staticcms/app",
"version": "1.2.10", "version": "1.2.12",
"license": "MIT", "license": "MIT",
"description": "Static CMS application.", "description": "Static CMS application.",
"repository": "https://github.com/StaticJsCMS/static-cms", "repository": "https://github.com/StaticJsCMS/static-cms",
@ -35,7 +35,7 @@
"@babel/eslint-parser": "7.19.1", "@babel/eslint-parser": "7.19.1",
"@babel/runtime": "7.21.0", "@babel/runtime": "7.21.0",
"@emotion/babel-preset-css-prop": "11.10.0", "@emotion/babel-preset-css-prop": "11.10.0",
"@staticcms/core": "^1.2.10", "@staticcms/core": "^1.2.12",
"buffer": "6.0.3", "buffer": "6.0.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
@ -57,11 +57,11 @@
"@babel/preset-typescript": "7.21.0", "@babel/preset-typescript": "7.21.0",
"@emotion/eslint-plugin": "11.10.0", "@emotion/eslint-plugin": "11.10.0",
"@emotion/jest": "11.10.5", "@emotion/jest": "11.10.5",
"@types/node": "16.18.12", "@types/node": "16.18.13",
"@types/react": "18.0.28", "@types/react": "18.0.28",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
"@typescript-eslint/eslint-plugin": "5.53.0", "@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.54.0",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",
"babel-plugin-emotion": "11.0.0", "babel-plugin-emotion": "11.0.0",

View File

@ -1,6 +1,6 @@
{ {
"name": "@staticcms/core", "name": "@staticcms/core",
"version": "1.2.10", "version": "1.2.12",
"license": "MIT", "license": "MIT",
"description": "Static CMS core application.", "description": "Static CMS core application.",
"repository": "https://github.com/StaticJsCMS/static-cms", "repository": "https://github.com/StaticJsCMS/static-cms",
@ -54,7 +54,7 @@
"@codemirror/language": "6.6.0", "@codemirror/language": "6.6.0",
"@codemirror/language-data": "6.1.0", "@codemirror/language-data": "6.1.0",
"@codemirror/legacy-modes": "6.3.1", "@codemirror/legacy-modes": "6.3.1",
"@codemirror/lint": "6.1.1", "@codemirror/lint": "6.2.0",
"@codemirror/search": "6.2.3", "@codemirror/search": "6.2.3",
"@codemirror/state": "6.2.0", "@codemirror/state": "6.2.0",
"@codemirror/theme-one-dark": "6.1.1", "@codemirror/theme-one-dark": "6.1.1",
@ -76,9 +76,9 @@
"@reduxjs/toolkit": "1.9.3", "@reduxjs/toolkit": "1.9.3",
"@styled-icons/fluentui-system-regular": "10.47.0", "@styled-icons/fluentui-system-regular": "10.47.0",
"@styled-icons/remix-editor": "10.46.0", "@styled-icons/remix-editor": "10.46.0",
"@udecode/plate": "19.6.0", "@udecode/plate": "19.7.0",
"@udecode/plate-juice": "19.5.0", "@udecode/plate-juice": "19.7.0",
"@udecode/plate-serializer-md": "19.5.0", "@udecode/plate-serializer-md": "19.7.0",
"@uiw/codemirror-extensions-langs": "4.19.9", "@uiw/codemirror-extensions-langs": "4.19.9",
"@uiw/react-codemirror": "4.19.9", "@uiw/react-codemirror": "4.19.9",
"ajv": "8.12.0", "ajv": "8.12.0",
@ -144,7 +144,7 @@
"slate": "0.91.4", "slate": "0.91.4",
"slate-history": "0.86.0", "slate-history": "0.86.0",
"slate-hyperscript": "0.77.0", "slate-hyperscript": "0.77.0",
"slate-react": "0.91.5", "slate-react": "0.91.6",
"stream-browserify": "3.0.0", "stream-browserify": "3.0.0",
"styled-components": "5.3.6", "styled-components": "5.3.6",
"symbol-observable": "4.0.0", "symbol-observable": "4.0.0",
@ -193,7 +193,7 @@
"@types/jwt-decode": "2.2.1", "@types/jwt-decode": "2.2.1",
"@types/lodash": "4.14.191", "@types/lodash": "4.14.191",
"@types/minimatch": "5.1.2", "@types/minimatch": "5.1.2",
"@types/node": "16.18.12", "@types/node": "16.18.13",
"@types/node-fetch": "2.6.2", "@types/node-fetch": "2.6.2",
"@types/react": "18.0.28", "@types/react": "18.0.28",
"@types/react-color": "3.0.6", "@types/react-color": "3.0.6",
@ -203,8 +203,8 @@
"@types/styled-components": "5.1.26", "@types/styled-components": "5.1.26",
"@types/url-join": "4.0.1", "@types/url-join": "4.0.1",
"@types/uuid": "9.0.1", "@types/uuid": "9.0.1",
"@typescript-eslint/eslint-plugin": "5.53.0", "@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.54.0",
"axios": "1.3.4", "axios": "1.3.4",
"babel-core": "7.0.0-bridge.0", "babel-core": "7.0.0-bridge.0",
"babel-loader": "9.1.2", "babel-loader": "9.1.2",

View File

@ -1,2 +0,0 @@
/* eslint-disable import/prefer-default-export */
export const createHashHistory = jest.fn();

View File

@ -1,18 +0,0 @@
import { history } from '../routing/history';
import { getCollectionUrl, getNewEntryUrl } from '../lib/urlHelper';
export function searchCollections(query: string, collection?: string) {
if (collection) {
history.push(`/collections/${collection}/search/${query}`);
} else {
history.push(`/search/${query}`);
}
}
export function showCollection(collectionName: string) {
history.push(getCollectionUrl(collectionName));
}
export function createNewEntry(collectionName: string) {
history.push(getNewEntryUrl(collectionName));
}

View File

@ -48,7 +48,6 @@ import {
selectIsFetching, selectIsFetching,
selectPublishedSlugs, selectPublishedSlugs,
} from '../reducers/selectors/entries'; } from '../reducers/selectors/entries';
import { navigateToEntry } from '../routing/history';
import { addSnackbar } from '../store/slices/snackbars'; import { addSnackbar } from '../store/slices/snackbars';
import { createAssetProxy } from '../valueObjects/AssetProxy'; import { createAssetProxy } from '../valueObjects/AssetProxy';
import createEntry from '../valueObjects/createEntry'; import createEntry from '../valueObjects/createEntry';
@ -56,6 +55,7 @@ import { addAssets, getAsset } from './media';
import { loadMedia, waitForMediaLibraryToLoad } from './mediaLibrary'; import { loadMedia, waitForMediaLibraryToLoad } from './mediaLibrary';
import { waitUntil } from './waitUntil'; import { waitUntil } from './waitUntil';
import type { NavigateFunction } from 'react-router-dom';
import type { AnyAction } from 'redux'; import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk'; import type { ThunkDispatch } from 'redux-thunk';
import type { Backend } from '../backend'; import type { Backend } from '../backend';
@ -950,7 +950,7 @@ export function getSerializedEntry(collection: Collection, entry: Entry): Entry
return serializedEntry; return serializedEntry;
} }
export function persistEntry(collection: Collection) { export function persistEntry(collection: Collection, navigate: NavigateFunction) {
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => { return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
const state = getState(); const state = getState();
const entryDraft = state.entryDraft; const entryDraft = state.entryDraft;
@ -1039,7 +1039,7 @@ export function persistEntry(collection: Collection) {
} }
if (entry.slug !== newSlug) { if (entry.slug !== newSlug) {
await dispatch(loadEntry(collection, newSlug)); await dispatch(loadEntry(collection, newSlug));
navigateToEntry(collection.name, newSlug); navigate(`/collections/${collection.name}/entries/${newSlug}`);
} else { } else {
await dispatch(loadEntry(collection, newSlug, true)); await dispatch(loadEntry(collection, newSlug, true));
} }

View File

@ -4,15 +4,23 @@ import { styled } from '@mui/material/styles';
import React, { useCallback, useEffect, useMemo } from 'react'; import React, { useCallback, useEffect, useMemo } from 'react';
import { translate } from 'react-polyglot'; import { translate } from 'react-polyglot';
import { connect } from 'react-redux'; 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 { ScrollSync } from 'react-scroll-sync';
import TopBarProgress from 'react-topbar-progress-indicator'; import TopBarProgress from 'react-topbar-progress-indicator';
import { loginUser as loginUserAction } from '@staticcms/core/actions/auth'; 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 { currentBackend } from '@staticcms/core/backend';
import { colors, GlobalStyles } from '@staticcms/core/components/UI/styles'; 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 { getDefaultPath } from '../../lib/util/collection.util';
import CollectionRoute from '../collection/CollectionRoute'; import CollectionRoute from '../collection/CollectionRoute';
import EditorRoute from '../editor/EditorRoute'; import EditorRoute from '../editor/EditorRoute';
@ -82,8 +90,10 @@ const App = ({
useMediaLibrary, useMediaLibrary,
t, t,
scrollSyncEnabled, scrollSyncEnabled,
discardDraft,
}: TranslatedProps<AppProps>) => { }: TranslatedProps<AppProps>) => {
const navigate = useNavigate();
const dispatch = useAppDispatch();
const configError = useCallback( const configError = useCallback(
(error?: string) => { (error?: string) => {
return ( return (
@ -140,22 +150,28 @@ const App = ({
base_url={config.config.backend.base_url} base_url={config.config.backend.base_url}
authEndpoint={config.config.backend.auth_endpoint} authEndpoint={config.config.backend.auth_endpoint}
config={config.config} config={config.config}
clearHash={() => history.replace('/')} clearHash={() => navigate('/', { replace: true })}
t={t} t={t}
/> />
</div> </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 defaultPath = useMemo(() => getDefaultPath(collections), [collections]);
const { pathname } = useLocation(); const { pathname } = useLocation();
const [searchParams] = useSearchParams();
useEffect(() => { useEffect(() => {
if (!/\/collections\/[a-zA-Z0-9_-]+\/entries\/[a-zA-Z0-9_-]+/g.test(pathname)) { if (
discardDraft(); /\/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(() => { const content = useMemo(() => {
if (!user) { if (!user) {
@ -265,7 +281,6 @@ function mapStateToProps(state: RootState) {
const mapDispatchToProps = { const mapDispatchToProps = {
loginUser: loginUserAction, loginUser: loginUserAction,
discardDraft: discardDraftAction,
}; };
const connector = connect(mapStateToProps, mapDispatchToProps); 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 React, { useCallback, useEffect, useMemo, useState } from 'react';
import { translate } from 'react-polyglot'; import { translate } from 'react-polyglot';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { useNavigate } from 'react-router-dom';
import { logoutUser as logoutUserAction } from '@staticcms/core/actions/auth'; 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 { openMediaLibrary as openMediaLibraryAction } from '@staticcms/core/actions/mediaLibrary';
import { checkBackendStatus as checkBackendStatusAction } from '@staticcms/core/actions/status'; import { checkBackendStatus as checkBackendStatusAction } from '@staticcms/core/actions/status';
import { buttons, colors } from '@staticcms/core/components/UI/styles'; 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 NavLink from '../UI/NavLink';
import SettingsDropdown from '../UI/SettingsDropdown'; import SettingsDropdown from '../UI/SettingsDropdown';
@ -82,9 +82,7 @@ const Header = ({
setAnchorEl(null); setAnchorEl(null);
}, []); }, []);
const handleCreatePostClick = useCallback((collectionName: string) => { const navigate = useNavigate();
createNewEntry(collectionName);
}, []);
const creatableCollections = useMemo( const creatableCollections = useMemo(
() => () =>
@ -148,7 +146,7 @@ const Header = ({
{creatableCollections.map(collection => ( {creatableCollections.map(collection => (
<MenuItem <MenuItem
key={collection.name} key={collection.name}
onClick={() => handleCreatePostClick(collection.name)} onClick={() => navigate(getNewEntryUrl(collection.name))}
> >
{collection.label_singular || collection.label} {collection.label_singular || collection.label}
</MenuItem> </MenuItem>

View File

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

View File

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

View File

@ -25,6 +25,7 @@ const EntriesSearch = ({
searchEntries, searchEntries,
clearSearch, clearSearch,
}: EntriesSearchProps) => { }: EntriesSearchProps) => {
console.log('collections', collections);
const collectionNames = useMemo(() => Object.keys(collections), [collections]); const collectionNames = useMemo(() => Object.keys(collections), [collections]);
const getCursor = useCallback(() => { const getCursor = useCallback(() => {
@ -80,6 +81,7 @@ function mapStateToProps(state: RootState, ownProps: EntriesSearchOwnProps) {
const isFetching = state.search.isFetching; const isFetching = state.search.isFetching;
const page = state.search.page; const page = state.search.page;
const entries = selectSearchedEntries(state, collectionNames); const entries = selectSearchedEntries(state, collectionNames);
console.log('searched entries', entries);
return { isFetching, page, collections, viewStyle, entries, searchTerm }; 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 Typography from '@mui/material/Typography';
import React, { useMemo } from 'react'; import React, { useMemo } from 'react';
import { translate } from 'react-polyglot'; 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 { colors } from '@staticcms/core/components/UI/styles';
import { getAdditionalLinks, getIcon } from '@staticcms/core/lib/registry'; import { getAdditionalLinks, getIcon } from '@staticcms/core/lib/registry';
import NavLink from '../UI/NavLink'; import NavLink from '../UI/NavLink';
@ -48,6 +48,15 @@ const Sidebar = ({
t, t,
filterTerm, filterTerm,
}: TranslatedProps<SidebarProps>) => { }: 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( const collectionLinks = useMemo(
() => () =>
Object.values(collections) Object.values(collections)

View File

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

View File

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

View File

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

View File

@ -156,9 +156,13 @@ export function selectDefaultSortableFields(collection: Collection, backend: Bac
} }
export function selectSortableFields( export function selectSortableFields(
collection: Collection, collection: Collection | undefined,
t: (key: string) => string, t: (key: string) => string,
): SortableField[] { ): SortableField[] {
if (!collection) {
return [];
}
const fields = (collection.sortable_fields?.fields ?? []) const fields = (collection.sortable_fields?.fields ?? [])
.map(key => { .map(key => {
if (key === COMMIT_DATE) { if (key === COMMIT_DATE) {
@ -177,12 +181,12 @@ export function selectSortableFields(
return fields; return fields;
} }
export function selectViewFilters(collection: Collection) { export function selectViewFilters(collection?: Collection) {
return collection.view_filters; return collection?.view_filters;
} }
export function selectViewGroups(collection: Collection) { export function selectViewGroups(collection?: Collection) {
return collection.view_groups; return collection?.view_groups;
} }
export function selectFieldsComments(collection: Collection, entryMap: Entry) { export function selectFieldsComments(collection: Collection, entryMap: Entry) {

View File

@ -21,7 +21,7 @@ const de: LocalePhrasesRoot = {
content: 'Inhalt', content: 'Inhalt',
workflow: 'Arbeitsablauf', workflow: 'Arbeitsablauf',
media: 'Medien', media: 'Medien',
quickAdd: 'Schnell-Erstellung', quickAdd: 'Schnellerstellung',
}, },
app: { app: {
errorHeader: 'Fehler beim Laden der CMS-Konfiguration.', errorHeader: 'Fehler beim Laden der CMS-Konfiguration.',
@ -36,8 +36,8 @@ const de: LocalePhrasesRoot = {
}, },
collection: { collection: {
sidebar: { sidebar: {
collections: 'Inhaltstypen', collections: 'Bereiche',
allCollections: 'Allen Inhaltstypen', allCollections: 'Allen Bereichen',
searchAll: 'Alles durchsuchen', searchAll: 'Alles durchsuchen',
searchIn: 'Suchen in', searchIn: 'Suchen in',
}, },
@ -231,6 +231,10 @@ const de: LocalePhrasesRoot = {
}, },
}, },
ui: { ui: {
common: {
yes: 'Ja',
no: 'Nein',
},
default: { default: {
goBackToSite: 'Zurück zur Seite', goBackToSite: 'Zurück zur Seite',
}, },

View File

@ -6,16 +6,28 @@ import type { CollectionViewStyle } from '@staticcms/core/constants/collectionVi
import type { Entry, Group, GroupMap, Sort } from '@staticcms/core/interface'; import type { Entry, Group, GroupMap, Sort } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store'; import type { RootState } from '@staticcms/core/store';
export function selectEntriesSort(entries: RootState, collection: string) { export function selectEntriesSort(entries: RootState, collection?: string) {
if (!collection) {
return undefined;
}
const sort = entries.entries.sort as Sort | undefined; const sort = entries.entries.sort as Sort | undefined;
return sort?.[collection]; return sort?.[collection];
} }
export const selectEntriesFilter = (collectionName: string) => (entries: RootState) => { export const selectEntriesFilter = (collectionName?: string) => (entries: RootState) => {
return entries.entries.filter?.[collectionName]; if (!collectionName) {
return {};
}
return entries.entries.filter?.[collectionName] ?? {};
}; };
export function selectEntriesGroup(entries: RootState, collection: string) { export function selectEntriesGroup(entries: RootState, collection?: string) {
if (!collection) {
return {};
}
const group = entries.entries.group as Group | undefined; const group = entries.entries.group as Group | undefined;
return group?.[collection] || {}; return group?.[collection] || {};
} }

View File

@ -1,17 +0,0 @@
import { createHashHistory } from 'history';
const history = createHashHistory();
export function navigateToCollection(collectionName: string) {
return history.push(`/collections/${collectionName}`);
}
export function navigateToNewEntry(collectionName: string) {
return history.replace(`/collections/${collectionName}/new`);
}
export function navigateToEntry(collectionName: string, slug: string) {
return history.replace(`/collections/${collectionName}/entries/${slug}`);
}
export { history };

View File

@ -38,13 +38,13 @@
"@next/bundle-analyzer": "13.2.1", "@next/bundle-analyzer": "13.2.1",
"@next/eslint-plugin-next": "13.2.1", "@next/eslint-plugin-next": "13.2.1",
"@types/js-yaml": "4.0.5", "@types/js-yaml": "4.0.5",
"@types/node": "16.18.12", "@types/node": "16.18.13",
"@types/prettier": "2.7.2", "@types/prettier": "2.7.2",
"@types/prismjs": "1.26.0", "@types/prismjs": "1.26.0",
"@types/react": "18.0.28", "@types/react": "18.0.28",
"@types/react-dom": "18.0.11", "@types/react-dom": "18.0.11",
"@typescript-eslint/eslint-plugin": "5.53.0", "@typescript-eslint/eslint-plugin": "5.54.0",
"@typescript-eslint/parser": "5.53.0", "@typescript-eslint/parser": "5.54.0",
"babel-eslint": "10.1.0", "babel-eslint": "10.1.0",
"eslint": "8.35.0", "eslint": "8.35.0",
"eslint-config-next": "13.2.1", "eslint-config-next": "13.2.1",

1165
yarn.lock

File diff suppressed because it is too large Load Diff