diff --git a/BREAKING_CHANGES.md b/BREAKING_CHANGES.md
new file mode 100644
index 00000000..81328ae4
--- /dev/null
+++ b/BREAKING_CHANGES.md
@@ -0,0 +1 @@
+- useMediaInsert now requires collection to be passed
\ No newline at end of file
diff --git a/packages/core/dev-test/backends/proxy/_data/settings.json b/packages/core/dev-test/backends/proxy/_data/settings.json
index a3ff5a78..4b8b8e2f 100644
--- a/packages/core/dev-test/backends/proxy/_data/settings.json
+++ b/packages/core/dev-test/backends/proxy/_data/settings.json
@@ -1,9 +1,9 @@
{
- "front_limit": 5,
+ "front_limit": 6,
"site_title": "Test",
"posts": {
"front_limit": 4,
"author": "Bob",
"thumb": "/backends/proxy/assets/upload/kanefreeman_2.jpg"
}
-}
+}
\ No newline at end of file
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something.md b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something/index.md
similarity index 57%
rename from packages/core/dev-test/backends/proxy/_posts/2022-11-01-something.md
rename to packages/core/dev-test/backends/proxy/_posts/2022-11-01-something/index.md
index 625f939e..d29b700b 100644
--- a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something.md
+++ b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something/index.md
@@ -2,7 +2,7 @@
title: Something something something...
draft: false
date: 2022-11-01 06:30
-image: /backends/proxy/assets/posts/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg
+image: ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg
---
# Welcome
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg
new file mode 100644
index 00000000..fd99d1ea
Binary files /dev/null and b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-something/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg differ
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test.md b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test.md
deleted file mode 100644
index 69126dc1..00000000
--- a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-title: Test
-draft: false
-date: 2022-11-01 14:28
-image: /backends/proxy/assets/posts/kittens.jpg
----
-Test2
-
-
-![moby-dick.jpg](/assets/upload/moby-dick.jpg)
-![moby-dick.jpg](/assets/upload/moby-dick.jpg)
-
-![kanefreeman_2.jpg](/assets/upload/kanefreeman_2.jpg)
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/index.md b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/index.md
new file mode 100644
index 00000000..0e0dcf88
--- /dev/null
+++ b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/index.md
@@ -0,0 +1,11 @@
+---
+title: Test
+draft: false
+date: 2022-11-01 14:28
+image: kittens.jpg
+---
+Test234t6
+
+[Test](https://example.com)
+
+![Kittens!](kittens.jpg)
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/kittens.jpg b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/kittens.jpg
new file mode 100644
index 00000000..a036ef99
Binary files /dev/null and b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/kittens.jpg differ
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/moby-dick.jpg b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/moby-dick.jpg
new file mode 100644
index 00000000..3234c5c9
Binary files /dev/null and b/packages/core/dev-test/backends/proxy/_posts/2022-11-01-test/moby-dick.jpg differ
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test.md b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test.md
deleted file mode 100644
index f9ff943f..00000000
--- a/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test.md
+++ /dev/null
@@ -1,7 +0,0 @@
----
-title: Test3
-draft: false
-date: 2022-11-02 08:43
-image: /backends/proxy/assets/upload/kanefreeman_2.jpg
----
-test25555
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/index.md b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/index.md
new file mode 100644
index 00000000..9e6f8dc9
--- /dev/null
+++ b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/index.md
@@ -0,0 +1,7 @@
+---
+title: Test3
+draft: false
+date: 2022-11-02 08:43
+image: ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg
+---
+test2555556
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/kanefreeman_2.jpg b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/kanefreeman_2.jpg
new file mode 100644
index 00000000..6acd26df
Binary files /dev/null and b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/kanefreeman_2.jpg differ
diff --git a/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg
new file mode 100644
index 00000000..fd99d1ea
Binary files /dev/null and b/packages/core/dev-test/backends/proxy/_posts/2022-11-02-test/ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg differ
diff --git a/packages/core/dev-test/backends/proxy/assets/upload/dalek.png b/packages/core/dev-test/backends/proxy/assets/upload/dalek.png
deleted file mode 100644
index a2415581..00000000
Binary files a/packages/core/dev-test/backends/proxy/assets/upload/dalek.png and /dev/null differ
diff --git a/packages/core/dev-test/backends/proxy/config.yml b/packages/core/dev-test/backends/proxy/config.yml
index 1c8cade0..d4e926a7 100644
--- a/packages/core/dev-test/backends/proxy/config.yml
+++ b/packages/core/dev-test/backends/proxy/config.yml
@@ -24,14 +24,15 @@ collections:
- name: posts
label: Posts
label_singular: Post
- media_folder: /packages/core/dev-test/backends/proxy/assets/posts
- public_folder: /backends/proxy/assets/posts
description: >
The description is a great place for tone setting, high level information,
and editing guidelines that are specific to a collection.
folder: packages/core/dev-test/backends/proxy/_posts
- slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
+ slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
+ media_folder: ""
+ public_folder: ""
+ path: "{{year}}-{{month}}-{{day}}-{{slug}}/index"
sortable_fields:
fields:
- title
diff --git a/packages/core/src/actions/entries.ts b/packages/core/src/actions/entries.ts
index 02944e60..ed498eb9 100644
--- a/packages/core/src/actions/entries.ts
+++ b/packages/core/src/actions/entries.ts
@@ -44,7 +44,7 @@ import { Cursor } from '../lib/util';
import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors';
import {
- selectEntriesSortFields,
+ selectEntriesSortField,
selectIsFetching,
selectPublishedSlugs,
} from '../reducers/selectors/entries';
@@ -653,10 +653,9 @@ export function loadEntries(collection: Collection, page = 0) {
return;
}
const state = getState();
- const sortFields = selectEntriesSortFields(state, collection.name);
- if (sortFields && sortFields.length > 0) {
- const field = sortFields[0];
- return dispatch(sortByField(collection, field.key, field.direction));
+ const sortField = selectEntriesSortField(collection.name)(state);
+ if (sortField) {
+ return dispatch(sortByField(collection, sortField.key, sortField.direction));
}
const configState = state.config;
@@ -956,7 +955,7 @@ export function persistEntry(collection: Collection) {
const state = getState();
const entryDraft = state.entryDraft;
const fieldsErrors = entryDraft.fieldsErrors;
- const usedSlugs = selectPublishedSlugs(state, collection.name);
+ const usedSlugs = selectPublishedSlugs(collection.name)(state);
// Early return if draft contains validation errors
if (Object.keys(fieldsErrors).length > 0) {
diff --git a/packages/core/src/actions/media.ts b/packages/core/src/actions/media.ts
index 4fc9be0f..f330b21c 100644
--- a/packages/core/src/actions/media.ts
+++ b/packages/core/src/actions/media.ts
@@ -6,11 +6,9 @@ import {
LOAD_ASSET_SUCCESS,
REMOVE_ASSET,
} from '../constants';
-import { isAbsolutePath } from '../lib/util';
import { selectMediaFilePath } from '../lib/util/media.util';
-import { selectMediaFileByPath } from '../reducers/selectors/mediaLibrary';
import { createAssetProxy } from '../valueObjects/AssetProxy';
-import { getMediaDisplayURL, getMediaFile, waitForMediaLibraryToLoad } from './mediaLibrary';
+import { getMediaFile, waitForMediaLibraryToLoad } from './mediaLibrary';
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
@@ -58,18 +56,9 @@ async function loadAsset(
dispatch(loadAssetRequest(resolvedPath));
// load asset url from backend
await waitForMediaLibraryToLoad(dispatch, getState());
- const file = selectMediaFileByPath(getState(), resolvedPath);
-
- let asset: AssetProxy;
- if (file) {
- const url = await getMediaDisplayURL(dispatch, getState(), file);
- asset = createAssetProxy({ path: resolvedPath, url: url || resolvedPath });
- dispatch(addAsset(asset));
- } else {
- const { url } = await getMediaFile(getState(), resolvedPath);
- asset = createAssetProxy({ path: resolvedPath, url });
- dispatch(addAsset(asset));
- }
+ const { url } = await getMediaFile(getState(), resolvedPath);
+ const asset = createAssetProxy({ path: resolvedPath, url });
+ dispatch(addAsset(asset));
dispatch(loadAssetSuccess(resolvedPath));
return asset;
} catch (error: unknown) {
@@ -105,7 +94,8 @@ export function getAsset(
path,
field as Field,
);
- let { asset, isLoading, error } = state.medias[resolvedPath] || {};
+
+ const { asset, isLoading } = state.medias[resolvedPath] || {};
if (isLoading) {
return promiseCache[resolvedPath];
}
@@ -116,23 +106,9 @@ export function getAsset(
}
const p = new Promise(resolve => {
- if (isAbsolutePath(resolvedPath)) {
- // asset path is a public url so we can just use it as is
- asset = createAssetProxy({ path: resolvedPath, url: path });
- dispatch(addAsset(asset));
+ loadAsset(resolvedPath, dispatch, getState).then(asset => {
resolve(asset);
- } else {
- if (error) {
- // on load error default back to original path
- asset = createAssetProxy({ path: resolvedPath, url: path });
- dispatch(addAsset(asset));
- resolve(asset);
- } else {
- loadAsset(resolvedPath, dispatch, getState).then(asset => {
- resolve(asset);
- });
- }
- }
+ });
});
promiseCache[resolvedPath] = p;
diff --git a/packages/core/src/actions/mediaLibrary.ts b/packages/core/src/actions/mediaLibrary.ts
index ff084792..4e04e9d4 100644
--- a/packages/core/src/actions/mediaLibrary.ts
+++ b/packages/core/src/actions/mediaLibrary.ts
@@ -34,6 +34,7 @@ import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type {
BaseField,
+ Collection,
DisplayURLState,
Field,
ImplementationMediaFile,
@@ -83,16 +84,28 @@ export function openMediaLibrary(
allowMultiple?: boolean;
replaceIndex?: number;
config?: Record;
+ collection?: Collection;
field?: F;
} = {},
) {
return (dispatch: ThunkDispatch, getState: () => RootState) => {
const state = getState();
const mediaLibrary = state.mediaLibrary.externalLibrary;
- const { controlID, value, config = {}, allowMultiple, forImage, replaceIndex, field } = payload;
+ const {
+ controlID,
+ value,
+ config = {},
+ allowMultiple,
+ forImage,
+ replaceIndex,
+ collection,
+ field,
+ } = payload;
+
if (mediaLibrary) {
mediaLibrary.show({ id: controlID, value, config, allowMultiple, imagesOnly: forImage });
}
+
dispatch(
mediaLibraryOpened({
controlID,
@@ -101,6 +114,7 @@ export function openMediaLibrary(
allowMultiple,
replaceIndex,
config,
+ collection: collection as Collection,
field: field as Field,
}),
);
@@ -406,6 +420,7 @@ function mediaLibraryOpened(payload: {
replaceIndex?: number;
allowMultiple?: boolean;
config?: Record;
+ collection?: Collection;
field?: Field;
}) {
return { type: MEDIA_LIBRARY_OPEN, payload } as const;
diff --git a/packages/core/src/components/App/App.tsx b/packages/core/src/components/App/App.tsx
index 2b55c80f..f7ff714e 100644
--- a/packages/core/src/components/App/App.tsx
+++ b/packages/core/src/components/App/App.tsx
@@ -14,8 +14,8 @@ import { currentBackend } from '@staticcms/core/backend';
import { colors, GlobalStyles } from '@staticcms/core/components/UI/styles';
import { history } from '@staticcms/core/routing/history';
import { getDefaultPath } from '../../lib/util/collection.util';
-import CollectionRoute from '../Collection/CollectionRoute';
-import EditorRoute from '../Editor/EditorRoute';
+import CollectionRoute from '../collection/CollectionRoute';
+import EditorRoute from '../editor/EditorRoute';
import MediaLibrary from '../MediaLibrary/MediaLibrary';
import Page from '../page/Page';
import Snackbars from '../snackbar/Snackbars';
@@ -183,7 +183,7 @@ const App = ({
element={}
/>
}
/>
{'collection' in otherProps ? (
) : (
,
- t: t,
-) {
- return groups.map(group => {
- const title = getGroupTitle(group, t);
- return (
-
- {title}
-
-
- );
- });
-}
-
-interface EntriesToRenderProps {
- entries: Entry[];
-}
-
-const EntriesCollection = ({
- collection,
- entries,
- groups,
- isFetching,
- viewStyle,
- cursor,
- page,
- traverseCollectionCursor,
- t,
- entriesLoaded,
- readyToLoad,
- loadEntries,
-}: TranslatedProps) => {
- const [prevReadyToLoad, setPrevReadyToLoad] = useState(false);
- const [prevCollection, setPrevCollection] = useState(collection);
-
- useEffect(() => {
- if (
- collection &&
- !entriesLoaded &&
- readyToLoad &&
- (!prevReadyToLoad || prevCollection !== collection)
- ) {
- loadEntries(collection);
- }
-
- setPrevReadyToLoad(readyToLoad);
- setPrevCollection(collection);
- }, [collection, entriesLoaded, loadEntries, prevCollection, prevReadyToLoad, readyToLoad]);
-
- const handleCursorActions = useCallback(
- (action: string) => {
- traverseCollectionCursor(collection, action);
- },
- [collection, traverseCollectionCursor],
- );
-
- const EntriesToRender = useCallback(
- ({ entries }: EntriesToRenderProps) => {
- return (
-
- );
- },
- [collection, cursor, handleCursorActions, isFetching, page, viewStyle],
- );
-
- if (groups && groups.length > 0) {
- return <>{withGroups(groups, entries, EntriesToRender, t)}>;
- }
-
- return ;
-};
-
export function filterNestedEntries(path: string, collectionFolder: string, entries: Entry[]) {
const filtered = entries.filter(e => {
const entryPath = e.path.slice(collectionFolder.length + 1);
@@ -152,6 +63,94 @@ export function filterNestedEntries(path: string, collectionFolder: string, entr
return filtered;
}
+const EntriesCollection = ({
+ collection,
+ filterTerm,
+ isFetching,
+ viewStyle,
+ cursor,
+ page,
+ t,
+ entriesLoaded,
+ readyToLoad,
+}: TranslatedProps) => {
+ const dispatch = useAppDispatch();
+
+ const [prevReadyToLoad, setPrevReadyToLoad] = useState(false);
+ const [prevCollection, setPrevCollection] = useState(collection);
+
+ const groups = useGroups(collection.name);
+
+ const entries = useEntries(collection);
+
+ const filteredEntries = useMemo(() => {
+ if ('nested' in collection) {
+ const collectionFolder = collection.folder ?? '';
+ return filterNestedEntries(filterTerm || '', collectionFolder, entries);
+ }
+
+ return entries;
+ }, [collection, entries, filterTerm]);
+
+ useEffect(() => {
+ if (
+ collection &&
+ !entriesLoaded &&
+ readyToLoad &&
+ (!prevReadyToLoad || prevCollection !== collection)
+ ) {
+ dispatch(loadEntries(collection));
+ }
+
+ setPrevReadyToLoad(readyToLoad);
+ setPrevCollection(collection);
+ }, [collection, dispatch, entriesLoaded, prevCollection, prevReadyToLoad, readyToLoad]);
+
+ const handleCursorActions = useCallback(
+ (action: string) => {
+ dispatch(traverseCollectionCursor(collection, action));
+ },
+ [collection, dispatch],
+ );
+
+ if (groups && groups.length > 0) {
+ <>
+ {groups.map(group => {
+ const title = getGroupTitle(group, t);
+ return (
+
+ {title}
+
+
+ );
+ })}
+ >;
+ }
+
+ return (
+
+ );
+};
+
interface EntriesCollectionOwnProps {
collection: Collection;
viewStyle: CollectionViewStyle;
@@ -163,27 +162,16 @@ function mapStateToProps(state: RootState, ownProps: EntriesCollectionOwnProps)
const { collection, viewStyle, filterTerm } = ownProps;
const page = state.entries.pages[collection.name]?.page;
- let entries = selectEntries(state, collection);
- const groups = selectGroups(state, collection);
-
- if ('nested' in collection) {
- const collectionFolder = collection.folder ?? '';
- entries = filterNestedEntries(filterTerm || '', collectionFolder, entries);
- }
-
const entriesLoaded = selectEntriesLoaded(state, collection.name);
const isFetching = selectIsFetching(state, collection.name);
const rawCursor = selectCollectionEntriesCursor(state, collection.name);
const cursor = Cursor.create(rawCursor).clearData();
- return { ...ownProps, page, entries, groups, entriesLoaded, isFetching, viewStyle, cursor };
+ return { ...ownProps, page, filterTerm, entriesLoaded, isFetching, viewStyle, cursor };
}
-const mapDispatchToProps = {
- loadEntries: loadEntriesAction,
- traverseCollectionCursor: traverseCollectionCursorAction,
-};
+const mapDispatchToProps = {};
const connector = connect(mapStateToProps, mapDispatchToProps);
export type EntriesCollectionProps = ConnectedProps;
diff --git a/packages/core/src/components/Collection/Entries/EntryCard.tsx b/packages/core/src/components/Collection/Entries/EntryCard.tsx
index e102ed7a..9c5bc721 100644
--- a/packages/core/src/components/Collection/Entries/EntryCard.tsx
+++ b/packages/core/src/components/Collection/Entries/EntryCard.tsx
@@ -4,10 +4,8 @@ import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import React, { useMemo } from 'react';
-import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
-import { getAsset as getAssetAction } from '@staticcms/core/actions/media';
import { VIEW_STYLE_GRID, VIEW_STYLE_LIST } from '@staticcms/core/constants/collectionViews';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import { getPreviewCard } from '@staticcms/core/lib/registry';
@@ -17,28 +15,58 @@ import {
selectTemplateName,
} from '@staticcms/core/lib/util/collection.util';
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
-import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias';
import { useAppSelector } from '@staticcms/core/store/hooks';
import useWidgetsFor from '../../common/widget/useWidgetsFor';
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
-import type { Collection, Entry, Field } from '@staticcms/core/interface';
-import type { RootState } from '@staticcms/core/store';
-import type { ConnectedProps } from 'react-redux';
+import type { Collection, Entry, FileOrImageField, MediaField } from '@staticcms/core/interface';
+
+export interface EntryCardProps {
+ entry: Entry;
+ imageFieldName?: string | null | undefined;
+ collection: Collection;
+ collectionLabel?: string;
+ viewStyle?: CollectionViewStyle;
+}
const EntryCard = ({
collection,
entry,
- path,
- image,
- imageField,
collectionLabel,
viewStyle = VIEW_STYLE_LIST,
-}: NestedCollectionProps) => {
+ imageFieldName,
+}: EntryCardProps) => {
+ const entryData = entry.data;
+
+ const path = useMemo(
+ () => `/collections/${collection.name}/entries/${entry.slug}`,
+ [collection.name, entry.slug],
+ );
+
+ const imageField = useMemo(
+ () =>
+ 'fields' in collection
+ ? (collection.fields?.find(
+ f => f.name === imageFieldName && f.widget === 'image',
+ ) as FileOrImageField)
+ : undefined,
+ [collection, imageFieldName],
+ );
+
+ const image = useMemo(() => {
+ let i = imageFieldName ? (entryData?.[imageFieldName] as string | undefined) : undefined;
+
+ if (i) {
+ i = encodeURI(i.trim());
+ }
+
+ return i;
+ }, [entryData, imageFieldName]);
+
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
const fields = selectFields(collection, entry.slug);
- const imageUrl = useMediaAsset(image, collection, imageField, entry);
+ const imageUrl = useMediaAsset(image, collection as Collection, imageField, entry);
const config = useAppSelector(selectConfig);
@@ -97,51 +125,4 @@ const EntryCard = ({
);
};
-interface EntryCardOwnProps {
- entry: Entry;
- inferredFields: {
- titleField?: string | null | undefined;
- descriptionField?: string | null | undefined;
- imageField?: string | null | undefined;
- remainingFields?: Field[] | undefined;
- };
- collection: Collection;
- imageField?: Field;
- collectionLabel?: string;
- viewStyle?: CollectionViewStyle;
-}
-
-function mapStateToProps(state: RootState, ownProps: EntryCardOwnProps) {
- const { entry, inferredFields, collection } = ownProps;
- const entryData = entry.data;
-
- let image = inferredFields.imageField
- ? (entryData?.[inferredFields.imageField] as string | undefined)
- : undefined;
-
- if (image) {
- image = encodeURI(image.trim());
- }
-
- const isLoadingAsset = selectIsLoadingAsset(state);
-
- return {
- ...ownProps,
- path: `/collections/${collection.name}/entries/${entry.slug}`,
- image,
- imageField:
- 'fields' in collection
- ? collection.fields?.find(f => f.name === inferredFields.imageField && f.widget === 'image')
- : undefined,
- isLoadingAsset,
- };
-}
-
-const mapDispatchToProps = {
- getAsset: getAssetAction,
-};
-
-const connector = connect(mapStateToProps, mapDispatchToProps);
-export type NestedCollectionProps = ConnectedProps;
-
-export default connector(EntryCard);
+export default EntryCard;
diff --git a/packages/core/src/components/Collection/Entries/EntryListing.tsx b/packages/core/src/components/Collection/Entries/EntryListing.tsx
index 17a63c07..eed436d1 100644
--- a/packages/core/src/components/Collection/Entries/EntryListing.tsx
+++ b/packages/core/src/components/Collection/Entries/EntryListing.tsx
@@ -8,7 +8,7 @@ import { selectFields, selectInferredField } from '@staticcms/core/lib/util/coll
import EntryCard from './EntryCard';
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
-import type { Field, Collection, Collections, Entry } from '@staticcms/core/interface';
+import type { Collection, Collections, Entry, Field } from '@staticcms/core/interface';
import type Cursor from '@staticcms/core/lib/util/Cursor';
interface CardsGridProps {
@@ -100,19 +100,19 @@ const EntryListing = ({
const renderedCards = useMemo(() => {
if ('collection' in otherProps) {
const inferredFields = inferFields(otherProps.collection);
- return entries.map((entry, idx) => (
+ return entries.map(entry => (
));
}
const isSingleCollectionInList = Object.keys(otherProps.collections).length === 1;
- return entries.map((entry, idx) => {
+ return entries.map(entry => {
const collectionName = entry.collection;
const collection = Object.values(otherProps.collections).find(
coll => coll.name === collectionName,
@@ -123,9 +123,9 @@ const EntryListing = ({
) : null;
});
diff --git a/packages/core/src/components/Collection/NestedCollection.tsx b/packages/core/src/components/Collection/NestedCollection.tsx
index 98b70438..47054e48 100644
--- a/packages/core/src/components/Collection/NestedCollection.tsx
+++ b/packages/core/src/components/Collection/NestedCollection.tsx
@@ -3,18 +3,15 @@ import { styled } from '@mui/material/styles';
import sortBy from 'lodash/sortBy';
import { dirname, sep } from 'path';
import React, { Fragment, useCallback, useEffect, useState } from 'react';
-import { connect } from 'react-redux';
import { NavLink } from 'react-router-dom';
import { colors, components } from '@staticcms/core/components/UI/styles';
import { transientOptions } from '@staticcms/core/lib';
+import useEntries from '@staticcms/core/lib/hooks/useEntries';
import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util';
import { stringTemplate } from '@staticcms/core/lib/widgets';
-import { selectEntries } from '@staticcms/core/reducers/selectors/entries';
import type { Collection, Entry } from '@staticcms/core/interface';
-import type { RootState } from '@staticcms/core/store';
-import type { ConnectedProps } from 'react-redux';
const { addFileTemplateFields } = stringTemplate;
@@ -271,7 +268,14 @@ export function updateNode(
return updater([...treeData]);
}
-const NestedCollection = ({ collection, entries, filterTerm }: NestedCollectionProps) => {
+export interface NestedCollectionProps {
+ collection: Collection;
+ filterTerm: string;
+}
+
+const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) => {
+ const entries = useEntries(collection);
+
const [treeData, setTreeData] = useState(getTreeData(collection, entries));
const [selected, setSelected] = useState(null);
const [useFilter, setUseFilter] = useState(true);
@@ -337,18 +341,4 @@ const NestedCollection = ({ collection, entries, filterTerm }: NestedCollectionP
return ;
};
-interface NestedCollectionOwnProps {
- collection: Collection;
- filterTerm: string;
-}
-
-function mapStateToProps(state: RootState, ownProps: NestedCollectionOwnProps) {
- const { collection } = ownProps;
- const entries = selectEntries(state, collection) ?? [];
- return { ...ownProps, entries };
-}
-
-const connector = connect(mapStateToProps, {});
-export type NestedCollectionProps = ConnectedProps;
-
-export default connector(NestedCollection);
+export default NestedCollection;
diff --git a/packages/core/src/components/Editor/EditorRoute.tsx b/packages/core/src/components/Editor/EditorRoute.tsx
index 9afccb40..ba2c3c77 100644
--- a/packages/core/src/components/Editor/EditorRoute.tsx
+++ b/packages/core/src/components/Editor/EditorRoute.tsx
@@ -12,7 +12,9 @@ interface EditorRouteProps {
}
const EditorRoute = ({ newRecord = false, collections }: EditorRouteProps) => {
- const { name, slug } = useParams();
+ const { name, ...params } = useParams();
+ const slug = params['*'];
+
const shouldRedirect = useMemo(() => {
if (!name) {
return false;
diff --git a/packages/core/src/components/MediaLibrary/MediaLibrary.tsx b/packages/core/src/components/MediaLibrary/MediaLibrary.tsx
index d9be425c..5640ead6 100644
--- a/packages/core/src/components/MediaLibrary/MediaLibrary.tsx
+++ b/packages/core/src/components/MediaLibrary/MediaLibrary.tsx
@@ -1,6 +1,5 @@
import fuzzy from 'fuzzy';
import React, { useCallback, useEffect, useRef, useState } from 'react';
-import { translate } from 'react-polyglot';
import { connect } from 'react-redux';
import {
@@ -12,12 +11,13 @@ import {
persistMedia as persistMediaAction,
} from '@staticcms/core/actions/mediaLibrary';
import { fileExtension } from '@staticcms/core/lib/util';
+import MediaLibraryCloseEvent from '@staticcms/core/lib/util/events/MediaLibraryCloseEvent';
import { selectMediaFiles } from '@staticcms/core/reducers/selectors/mediaLibrary';
import alert from '../UI/Alert';
import confirm from '../UI/Confirm';
import MediaLibraryModal from './MediaLibraryModal';
-import type { MediaFile, TranslatedProps } from '@staticcms/core/interface';
+import type { MediaFile } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
import type { ChangeEvent, KeyboardEvent } from 'react';
import type { ConnectedProps } from 'react-redux';
@@ -53,7 +53,7 @@ const MediaLibrary = ({
isDeleting,
hasNextPage,
isPaginating,
- config,
+ config: mediaConfig,
loadMedia,
dynamicSearchQuery,
page,
@@ -61,31 +61,28 @@ const MediaLibrary = ({
deleteMedia,
insertMedia,
closeMediaLibrary,
+ collection,
field,
- t,
-}: TranslatedProps) => {
+}: MediaLibraryProps) => {
const [selectedFile, setSelectedFile] = useState(null);
const [query, setQuery] = useState(undefined);
const [prevIsVisible, setPrevIsVisible] = useState(false);
useEffect(() => {
- loadMedia();
+ loadMedia({});
}, [loadMedia]);
useEffect(() => {
if (!prevIsVisible && isVisible) {
setSelectedFile(null);
setQuery('');
+ loadMedia();
+ } else if (prevIsVisible && !isVisible) {
+ window.dispatchEvent(new MediaLibraryCloseEvent());
}
setPrevIsVisible(isVisible);
- }, [isVisible, prevIsVisible]);
-
- useEffect(() => {
- if (!prevIsVisible && isVisible) {
- loadMedia();
- }
}, [isVisible, loadMedia, prevIsVisible]);
const loadDisplayURL = useCallback(
@@ -155,7 +152,7 @@ const MediaLibrary = ({
[selectedFile?.key],
);
- const scrollContainerRef = useRef();
+ const scrollContainerRef = useRef(null);
const scrollToTop = () => {
if (scrollContainerRef.current) {
scrollContainerRef.current.scrollTop = 0;
@@ -189,7 +186,8 @@ const MediaLibrary = ({
event.preventDefault();
const files = [...Array.from(fileList)];
const file = files[0];
- const maxFileSize = typeof config.max_file_size === 'number' ? config.max_file_size : 512000;
+ const maxFileSize =
+ typeof mediaConfig.max_file_size === 'number' ? mediaConfig.max_file_size : 512000;
if (maxFileSize && file.size > maxFileSize) {
alert({
@@ -213,7 +211,7 @@ const MediaLibrary = ({
event.target.value = '';
}
},
- [config.max_file_size, field, persistMedia],
+ [mediaConfig.max_file_size, field, persistMedia],
);
/**
@@ -310,7 +308,7 @@ const MediaLibrary = ({
/**
* Filters files that do not match the query. Not used for dynamic search.
*/
- const queryFilter = useCallback((query: string, files: { name: string }[]) => {
+ const queryFilter = useCallback((query: string, files: MediaFile[]): MediaFile[] => {
/**
* Because file names don't have spaces, typing a space eliminates all
* potential matches, so we strip them all out internally before running the
@@ -318,11 +316,10 @@ const MediaLibrary = ({
*/
const strippedQuery = query.replace(/ /g, '');
const matches = fuzzy.filter(strippedQuery, files, { extract: file => file.name });
- const matchFiles = matches.map((match, queryIndex) => {
+ return matches.map((match, queryIndex) => {
const file = files[match.index];
return { ...file, queryIndex };
- });
- return matchFiles;
+ }) as MediaFile[];
}, []);
return (
@@ -339,7 +336,7 @@ const MediaLibrary = ({
hasNextPage={hasNextPage}
isPaginating={isPaginating}
query={query}
- selectedFile={selectedFile}
+ selectedFile={selectedFile ?? undefined}
handleFilter={filterImages}
handleQuery={queryFilter}
toTableData={toTableData}
@@ -350,12 +347,13 @@ const MediaLibrary = ({
handleDelete={handleDelete}
handleInsert={handleInsert}
handleDownload={handleDownload}
- setScrollContainerRef={scrollContainerRef}
+ scrollContainerRef={scrollContainerRef}
handleAssetClick={handleAssetClick}
handleLoadMore={handleLoadMore}
displayURLs={displayURLs}
loadDisplayURL={loadDisplayURL}
- t={t}
+ collection={collection}
+ field={field}
/>
);
};
@@ -379,6 +377,7 @@ function mapStateToProps(state: RootState) {
page: mediaLibrary.page,
hasNextPage: mediaLibrary.hasNextPage,
isPaginating: mediaLibrary.isPaginating,
+ collection: mediaLibrary.collection,
field,
};
return { ...mediaLibraryProps };
@@ -396,4 +395,4 @@ const mapDispatchToProps = {
const connector = connect(mapStateToProps, mapDispatchToProps);
export type MediaLibraryProps = ConnectedProps;
-export default connector(translate()(MediaLibrary));
+export default connector(MediaLibrary);
diff --git a/packages/core/src/components/MediaLibrary/MediaLibraryCard.tsx b/packages/core/src/components/MediaLibrary/MediaLibraryCard.tsx
index 7b0ae97d..b7cfa7a2 100644
--- a/packages/core/src/components/MediaLibrary/MediaLibraryCard.tsx
+++ b/packages/core/src/components/MediaLibrary/MediaLibraryCard.tsx
@@ -4,8 +4,11 @@ import React, { useEffect } from 'react';
import { borders, colors, effects, lengths, shadows } from '@staticcms/core/components/UI/styles';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import transientOptions from '@staticcms/core/lib/util/transientOptions';
+import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
+import { useAppSelector } from '@staticcms/core/store/hooks';
import type { MediaLibraryDisplayURL } from '@staticcms/core/reducers/mediaLibrary';
+import type { Field, Collection } from '@staticcms/core/interface';
const IMAGE_HEIGHT = 160;
@@ -89,6 +92,8 @@ interface MediaLibraryCardProps {
isViewableImage: boolean;
loadDisplayURL: () => void;
isDraft?: boolean;
+ collection?: Collection;
+ field?: Field;
}
const MediaLibraryCard = ({
@@ -103,9 +108,12 @@ const MediaLibraryCard = ({
type,
isViewableImage,
isDraft,
+ collection,
+ field,
loadDisplayURL,
}: MediaLibraryCardProps) => {
- const url = useMediaAsset(displayURL.url);
+ const entry = useAppSelector(selectEditingDraft);
+ const url = useMediaAsset(displayURL.url, collection, field, entry);
useEffect(() => {
if (!displayURL.url) {
diff --git a/packages/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx b/packages/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx
index 99aa0b9d..0d25e380 100644
--- a/packages/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx
+++ b/packages/core/src/components/MediaLibrary/MediaLibraryCardGrid.tsx
@@ -6,12 +6,12 @@ import { FixedSizeGrid as Grid } from 'react-window';
import MediaLibraryCard from './MediaLibraryCard';
-import type { GridChildComponentProps } from 'react-window';
-import type { MediaFile } from '@staticcms/core/interface';
+import type { Collection, Field, MediaFile } from '@staticcms/core/interface';
import type {
MediaLibraryDisplayURL,
MediaLibraryState,
} from '@staticcms/core/reducers/mediaLibrary';
+import type { GridChildComponentProps } from 'react-window';
export interface MediaLibraryCardItem {
displayURL?: MediaLibraryDisplayURL;
@@ -25,7 +25,7 @@ export interface MediaLibraryCardItem {
}
export interface MediaLibraryCardGridProps {
- setScrollContainerRef: () => void;
+ scrollContainerRef: React.MutableRefObject;
mediaItems: MediaFile[];
isSelectedFile: (file: MediaFile) => boolean;
onAssetClick: (asset: MediaFile) => void;
@@ -39,6 +39,8 @@ export interface MediaLibraryCardGridProps {
cardMargin: string;
loadDisplayURL: (asset: MediaFile) => void;
displayURLs: MediaLibraryState['displayURLs'];
+ collection?: Collection;
+ field?: Field;
}
export type CardGridItemData = MediaLibraryCardGridProps & {
@@ -61,6 +63,8 @@ const CardWrapper = ({
loadDisplayURL,
columnCount,
gutter,
+ collection,
+ field,
},
}: GridChildComponentProps) => {
const index = rowIndex * columnCount + columnIndex;
@@ -93,6 +97,8 @@ const CardWrapper = ({
loadDisplayURL={() => loadDisplayURL(file)}
type={file.type}
isViewableImage={file.isViewableImage ?? false}
+ collection={collection}
+ field={field}
/>
);
@@ -126,7 +132,7 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => {
cardHeight: inputCardHeight,
cardMargin,
mediaItems,
- setScrollContainerRef,
+ scrollContainerRef,
} = props;
return (
@@ -141,7 +147,7 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => {
const rowCount = Math.ceil(mediaItems.length / columnCount);
return (
-
+
{
};
const PaginatedGrid = ({
- setScrollContainerRef,
+ scrollContainerRef,
mediaItems,
isSelectedFile,
onAssetClick,
@@ -182,9 +188,11 @@ const PaginatedGrid = ({
onLoadMore,
isPaginating,
paginatingMessage,
+ collection,
+ field,
}: MediaLibraryCardGridProps) => {
return (
-
+
{mediaItems.map(file => (
loadDisplayURL(file)}
type={file.type}
isViewableImage={file.isViewableImage ?? false}
+ collection={collection}
+ field={field}
/>
))}
{!canLoadMore ? null : }
diff --git a/packages/core/src/components/MediaLibrary/MediaLibraryModal.tsx b/packages/core/src/components/MediaLibrary/MediaLibraryModal.tsx
index 12f9645b..b8fe049a 100644
--- a/packages/core/src/components/MediaLibrary/MediaLibraryModal.tsx
+++ b/packages/core/src/components/MediaLibrary/MediaLibraryModal.tsx
@@ -11,9 +11,9 @@ import EmptyMessage from './EmptyMessage';
import MediaLibraryCardGrid from './MediaLibraryCardGrid';
import MediaLibraryTop from './MediaLibraryTop';
-import type { ChangeEvent, ChangeEventHandler, KeyboardEventHandler } from 'react';
-import type { MediaFile, TranslatedProps } from '@staticcms/core/interface';
+import type { Collection, Field, MediaFile, TranslatedProps } from '@staticcms/core/interface';
import type { MediaLibraryState } from '@staticcms/core/reducers/mediaLibrary';
+import type { ChangeEvent, ChangeEventHandler, FC, KeyboardEventHandler } from 'react';
const StyledFab = styled(Fab)`
position: absolute;
@@ -95,11 +95,13 @@ interface MediaLibraryModalProps {
handleDelete: () => void;
handleInsert: () => void;
handleDownload: () => void;
- setScrollContainerRef: () => void;
+ scrollContainerRef: React.MutableRefObject;
handleAssetClick: (asset: MediaFile) => void;
handleLoadMore: () => void;
loadDisplayURL: (file: MediaFile) => void;
displayURLs: MediaLibraryState['displayURLs'];
+ collection?: Collection;
+ field?: Field;
}
const MediaLibraryModal = ({
@@ -126,11 +128,13 @@ const MediaLibraryModal = ({
handleDelete,
handleInsert,
handleDownload,
- setScrollContainerRef,
+ scrollContainerRef,
handleAssetClick,
handleLoadMore,
loadDisplayURL,
displayURLs,
+ collection,
+ field,
t,
}: TranslatedProps) => {
const filteredFiles = forImage ? handleFilter(files) : files;
@@ -177,7 +181,7 @@ const MediaLibraryModal = ({
{!shouldShowEmptyMessage ? null : }
selectedFile?.key === file.key}
onAssetClick={handleAssetClick}
@@ -191,10 +195,12 @@ const MediaLibraryModal = ({
cardMargin={cardMargin}
loadDisplayURL={loadDisplayURL}
displayURLs={displayURLs}
+ collection={collection}
+ field={field}
/>
);
};
-export default translate()(MediaLibraryModal);
+export default translate()(MediaLibraryModal) as FC;
diff --git a/packages/core/src/components/common/image/Image.tsx b/packages/core/src/components/common/image/Image.tsx
new file mode 100644
index 00000000..701b9bf7
--- /dev/null
+++ b/packages/core/src/components/common/image/Image.tsx
@@ -0,0 +1,35 @@
+import React from 'react';
+
+import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
+import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
+import { useAppSelector } from '@staticcms/core/store/hooks';
+
+import type { Collection, MediaField } from '@staticcms/core/interface';
+
+export interface ImageProps {
+ src?: string;
+ alt?: string;
+ collection?: Collection;
+ field?: F;
+}
+
+const Image = ({ src, alt, collection, field }: ImageProps) => {
+ const entry = useAppSelector(selectEditingDraft);
+
+ const assetSource = useMediaAsset(src, collection, field, entry);
+
+ return ;
+};
+
+export const withMdxImage = ({
+ collection,
+ field,
+}: Omit, 'src' | 'alt'>) => {
+ const MdxImage = ({ src, alt }: Pick, 'src' | 'alt'>) => (
+
+ );
+
+ return MdxImage;
+};
+
+export default Image;
diff --git a/packages/core/src/components/page/Page.tsx b/packages/core/src/components/page/Page.tsx
index 4a83528b..4bda71db 100644
--- a/packages/core/src/components/page/Page.tsx
+++ b/packages/core/src/components/page/Page.tsx
@@ -6,7 +6,7 @@ import { useParams } from 'react-router-dom';
import { getAdditionalLink } from '@staticcms/core/lib/registry';
import MainView from '../App/MainView';
-import Sidebar from '../Collection/Sidebar';
+import Sidebar from '../collection/Sidebar';
import type { ComponentType } from 'react';
import type { ConnectedProps } from 'react-redux';
diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts
index 804824a9..e8726bac 100644
--- a/packages/core/src/interface.ts
+++ b/packages/core/src/interface.ts
@@ -535,6 +535,12 @@ export interface BaseField {
widget: string;
}
+export interface MediaField extends BaseField {
+ media_library?: MediaLibrary;
+ media_folder?: string;
+ public_folder?: string;
+}
+
export interface BooleanField extends BaseField {
widget: 'boolean';
default?: boolean;
@@ -572,13 +578,9 @@ export interface DateTimeField extends BaseField {
picker_utc?: boolean;
}
-export interface FileOrImageField extends BaseField {
+export interface FileOrImageField extends MediaField {
widget: 'file' | 'image';
default?: string;
-
- media_library?: MediaLibrary;
- media_folder?: string;
- public_folder?: string;
}
export interface ObjectField extends BaseField {
@@ -614,13 +616,9 @@ export interface MapField extends BaseField {
height?: string;
}
-export interface MarkdownField extends BaseField {
+export interface MarkdownField extends MediaField {
widget: 'markdown';
default?: string;
-
- media_library?: MediaLibrary;
- media_folder?: string;
- public_folder?: string;
}
export interface NumberField extends BaseField {
diff --git a/packages/core/src/lib/hooks/useEntries.ts b/packages/core/src/lib/hooks/useEntries.ts
new file mode 100644
index 00000000..94dfc9db
--- /dev/null
+++ b/packages/core/src/lib/hooks/useEntries.ts
@@ -0,0 +1,51 @@
+import get from 'lodash/get';
+import orderBy from 'lodash/orderBy';
+import { useMemo } from 'react';
+
+import { SORT_DIRECTION_ASCENDING } from '@staticcms/core/constants';
+import { selectEntriesSortField } from '@staticcms/core/reducers/selectors/entries';
+import { useAppSelector } from '@staticcms/core/store/hooks';
+import { selectSortDataPath } from '../util/sort.util';
+import useFilters from './useFilters';
+import usePublishedEntries from './usePublishedEntries';
+
+import type { Collection } from '@staticcms/core/interface';
+
+export default function useEntries(collection: Collection) {
+ const entries = usePublishedEntries(collection.name);
+
+ const entriesSortFieldSelector = useMemo(
+ () => selectEntriesSortField(collection.name),
+ [collection.name],
+ );
+ const sortField = useAppSelector(entriesSortFieldSelector);
+
+ const filters = useFilters(collection.name);
+
+ return useMemo(() => {
+ let finalEntries = [...entries];
+
+ if (sortField) {
+ const key = selectSortDataPath(collection, sortField.key);
+ const order = sortField.direction === SORT_DIRECTION_ASCENDING ? 'asc' : 'desc';
+ finalEntries = orderBy(finalEntries, key, order);
+ }
+
+ if (filters && filters.length > 0) {
+ finalEntries = finalEntries.filter(e => {
+ const allMatched = filters.every(f => {
+ const pattern = f.pattern;
+ const field = f.field;
+ const data = e!.data || {};
+ const toMatch = get(data, field);
+ const matched =
+ toMatch !== undefined && new RegExp(String(pattern)).test(String(toMatch));
+ return matched;
+ });
+ return allMatched;
+ });
+ }
+
+ return finalEntries;
+ }, [collection, entries, filters, sortField]);
+}
diff --git a/packages/core/src/lib/hooks/useFilters.ts b/packages/core/src/lib/hooks/useFilters.ts
new file mode 100644
index 00000000..d1a177a9
--- /dev/null
+++ b/packages/core/src/lib/hooks/useFilters.ts
@@ -0,0 +1,16 @@
+import { useMemo } from 'react';
+
+import { selectEntriesSortField } from '@staticcms/core/reducers/selectors/entries';
+import { useAppSelector } from '@staticcms/core/store/hooks';
+
+export default function useFilters(collectionName: string) {
+ const entriesSortFieldSelector = useMemo(
+ () => selectEntriesSortField(collectionName),
+ [collectionName],
+ );
+ const filters = useAppSelector(entriesSortFieldSelector);
+
+ return useMemo(() => {
+ return Object.values(filters ?? {}).filter(v => v?.active === true) || [];
+ }, [filters]);
+}
diff --git a/packages/core/src/lib/hooks/useGroups.ts b/packages/core/src/lib/hooks/useGroups.ts
new file mode 100644
index 00000000..8f44578d
--- /dev/null
+++ b/packages/core/src/lib/hooks/useGroups.ts
@@ -0,0 +1,41 @@
+import groupBy from 'lodash/groupBy';
+import { useMemo } from 'react';
+
+import { getGroup, selectEntriesGroupField } from '@staticcms/core/reducers/selectors/entries';
+import { useAppSelector } from '@staticcms/core/store/hooks';
+import usePublishedEntries from './usePublishedEntries';
+
+import type { GroupOfEntries } from '@staticcms/core/interface';
+
+export default function useGroups(collectionName: string) {
+ const entries = usePublishedEntries(collectionName);
+
+ const entriesGroupFieldSelector = useMemo(
+ () => selectEntriesGroupField(collectionName),
+ [collectionName],
+ );
+ const selectedGroup = useAppSelector(entriesGroupFieldSelector);
+
+ return useMemo(() => {
+ if (selectedGroup === undefined) {
+ return [];
+ }
+
+ let groups: Record =
+ {};
+ const groupedEntries = groupBy(entries, entry => {
+ const group = getGroup(entry, selectedGroup);
+ groups = { ...groups, [group.id]: group };
+ return group.id;
+ });
+
+ const groupsArray: GroupOfEntries[] = Object.entries(groupedEntries).map(([id, entries]) => {
+ return {
+ ...groups[id],
+ paths: new Set(entries.map(entry => entry.path)),
+ };
+ });
+
+ return groupsArray;
+ }, [entries, selectedGroup]);
+}
diff --git a/packages/core/src/lib/hooks/useIsMediaAsset.ts b/packages/core/src/lib/hooks/useIsMediaAsset.ts
index 541675dd..b4d0b59a 100644
--- a/packages/core/src/lib/hooks/useIsMediaAsset.ts
+++ b/packages/core/src/lib/hooks/useIsMediaAsset.ts
@@ -2,11 +2,12 @@ import { useEffect, useState } from 'react';
import { emptyAsset, getAsset } from '@staticcms/core/actions/media';
import { useAppDispatch } from '@staticcms/core/store/hooks';
-import { isNotEmpty } from '../util/string.util';
+import { isEmpty, isNotEmpty } from '../util/string.util';
+import useDebounce from './useDebounce';
-import type { Collection, Entry, FileOrImageField, MarkdownField } from '@staticcms/core/interface';
+import type { Collection, Entry, MediaField } from '@staticcms/core/interface';
-export default function useIsMediaAsset(
+export default function useIsMediaAsset(
url: string,
collection: Collection,
field: T,
@@ -14,17 +15,22 @@ export default function useIsMediaAsset {
+ if (isEmpty(debouncedUrl)) {
+ return;
+ }
+
const checkMediaExistence = async () => {
- const asset = await dispatch(getAsset(collection, entry, url, field));
+ const asset = await dispatch(getAsset(collection, entry, debouncedUrl, field));
setExists(
Boolean(asset && asset !== emptyAsset && isNotEmpty(asset.toString()) && asset.fileObj),
);
};
checkMediaExistence();
- }, [collection, dispatch, entry, field, url]);
+ }, [collection, dispatch, entry, field, debouncedUrl]);
return exists;
}
diff --git a/packages/core/src/lib/hooks/useMediaAsset.ts b/packages/core/src/lib/hooks/useMediaAsset.ts
index ff96f63b..be6211f6 100644
--- a/packages/core/src/lib/hooks/useMediaAsset.ts
+++ b/packages/core/src/lib/hooks/useMediaAsset.ts
@@ -1,34 +1,39 @@
-import { useEffect, useState } from 'react';
+import { useEffect, useMemo, useState } from 'react';
import { emptyAsset, getAsset } from '@staticcms/core/actions/media';
import { useAppDispatch } from '@staticcms/core/store/hooks';
-import { isNotEmpty } from '../util/string.util';
+import useDebounce from './useDebounce';
-import type { Field, Collection, Entry } from '@staticcms/core/interface';
+import type { Collection, Entry, MediaField } from '@staticcms/core/interface';
-export default function useMediaAsset(
+export default function useMediaAsset(
url: string | undefined,
collection?: Collection,
field?: T,
entry?: Entry,
): string {
const dispatch = useAppDispatch();
- const [assetSource, setAssetSource] = useState(url);
+ const [assetSource, setAssetSource] = useState('');
+ const debouncedUrl = useDebounce(url, 200);
useEffect(() => {
- if (!url) {
+ if (!debouncedUrl) {
return;
}
const fetchMedia = async () => {
- const asset = await dispatch(getAsset(collection, entry, url, field));
+ const asset = await dispatch(getAsset(collection, entry, debouncedUrl, field));
if (asset !== emptyAsset) {
setAssetSource(asset?.toString() ?? '');
}
};
fetchMedia();
- }, [collection, dispatch, entry, field, url]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [debouncedUrl]);
- return isNotEmpty(assetSource) ? assetSource : url ?? '';
+ return useMemo(
+ () => (debouncedUrl?.startsWith('blob:') ? debouncedUrl : assetSource ?? ''),
+ [assetSource, debouncedUrl],
+ );
}
diff --git a/packages/core/src/lib/hooks/useMediaInsert.ts b/packages/core/src/lib/hooks/useMediaInsert.ts
index 39c9a7f2..6e44432e 100644
--- a/packages/core/src/lib/hooks/useMediaInsert.ts
+++ b/packages/core/src/lib/hooks/useMediaInsert.ts
@@ -5,17 +5,17 @@ import { openMediaLibrary, removeInsertedMedia } from '@staticcms/core/actions/m
import { selectMediaPath } from '@staticcms/core/reducers/selectors/mediaLibrary';
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
-import type { FileOrImageField, MarkdownField } from '@staticcms/core/interface';
+import type { Collection, MediaField } from '@staticcms/core/interface';
import type { MouseEvent } from 'react';
-export default function useMediaInsert(
+export default function useMediaInsert(
value: T,
- options: { field?: FileOrImageField | MarkdownField; controlID?: string; forImage?: boolean },
+ options: { collection: Collection; field: F; controlID?: string; forImage?: boolean },
callback: (newValue: T) => void,
): (e?: MouseEvent) => void {
const dispatch = useAppDispatch();
- const { controlID, field, forImage = false } = options;
+ const { controlID, collection, field, forImage = false } = options;
const finalControlID = useMemo(() => controlID ?? uuid(), [controlID]);
const mediaPathSelector = useMemo(() => selectMediaPath(finalControlID), [finalControlID]);
@@ -50,11 +50,12 @@ export default function useMediaInsert(
replaceIndex,
allowMultiple: false,
config,
+ collection,
field,
}),
);
},
- [dispatch, finalControlID, forImage, value, config, field],
+ [dispatch, finalControlID, forImage, value, config, collection, field],
);
return handleOpenMediaLibrary;
diff --git a/packages/core/src/lib/hooks/usePublishedEntries.ts b/packages/core/src/lib/hooks/usePublishedEntries.ts
new file mode 100644
index 00000000..73e9300b
--- /dev/null
+++ b/packages/core/src/lib/hooks/usePublishedEntries.ts
@@ -0,0 +1,22 @@
+import { useMemo } from 'react';
+
+import {
+ selectEntriesBySlugs,
+ selectPublishedSlugs,
+} from '@staticcms/core/reducers/selectors/entries';
+import { useAppSelector } from '@staticcms/core/store/hooks';
+
+export default function usePublishedEntries(collectionName: string) {
+ const publishedSlugsSelector = useMemo(
+ () => selectPublishedSlugs(collectionName),
+ [collectionName],
+ );
+
+ const slugs = useAppSelector(publishedSlugsSelector);
+ const entries = useAppSelector(selectEntriesBySlugs);
+
+ return useMemo(
+ () => slugs && slugs.map(slug => entries[`${collectionName}.${slug}`]),
+ [collectionName, entries, slugs],
+ );
+}
diff --git a/packages/core/src/lib/util/collection.util.ts b/packages/core/src/lib/util/collection.util.ts
index 36004ab4..9e6136ee 100644
--- a/packages/core/src/lib/util/collection.util.ts
+++ b/packages/core/src/lib/util/collection.util.ts
@@ -258,6 +258,7 @@ export function selectMediaFolders(config: Config, collection: Collection, entry
return [...new Set(folders)];
}
+
export function getFieldsNames(fields: Field[] | undefined, prefix = '') {
let names = fields?.map(f => `${prefix}${f.name}`) ?? [];
diff --git a/packages/core/src/lib/util/events/MediaLibraryCloseEvent.ts b/packages/core/src/lib/util/events/MediaLibraryCloseEvent.ts
new file mode 100644
index 00000000..2fedd718
--- /dev/null
+++ b/packages/core/src/lib/util/events/MediaLibraryCloseEvent.ts
@@ -0,0 +1,5 @@
+export default class MediaLibraryCloseEvent extends CustomEvent<{}> {
+ constructor() {
+ super('mediaLibraryClose', {});
+ }
+}
diff --git a/packages/core/src/lib/util/media.util.ts b/packages/core/src/lib/util/media.util.ts
index ad52ca12..77dc4b90 100644
--- a/packages/core/src/lib/util/media.util.ts
+++ b/packages/core/src/lib/util/media.util.ts
@@ -49,12 +49,12 @@ function hasCustomFolder(
if ('files' in collection) {
const file = getFileField(collection.files, slug);
- if (file && file[folderKey]) {
+ if (file && folderKey in file) {
return true;
}
}
- if (collection[folderKey]) {
+ if (folderKey in collection) {
return true;
}
@@ -72,7 +72,7 @@ function evaluateFolder(
const collection = { ...c };
// add identity template if doesn't exist
- if (!collection[folderKey]) {
+ if (!(folderKey in collection)) {
collection[folderKey] = `{{${folderKey}}}`;
}
diff --git a/packages/core/src/lib/util/window.util.ts b/packages/core/src/lib/util/window.util.ts
index 0adee83c..c66b4964 100644
--- a/packages/core/src/lib/util/window.util.ts
+++ b/packages/core/src/lib/util/window.util.ts
@@ -2,10 +2,12 @@ import { useEffect } from 'react';
import type AlertEvent from './events/AlertEvent';
import type ConfirmEvent from './events/ConfirmEvent';
+import type MediaLibraryCloseEvent from './events/MediaLibraryCloseEvent';
interface EventMap {
alert: AlertEvent;
confirm: ConfirmEvent;
+ mediaLibraryClose: MediaLibraryCloseEvent;
}
export function useWindowEvent(
diff --git a/packages/core/src/reducers/mediaLibrary.ts b/packages/core/src/reducers/mediaLibrary.ts
index 7a6890da..13e9fd0f 100644
--- a/packages/core/src/reducers/mediaLibrary.ts
+++ b/packages/core/src/reducers/mediaLibrary.ts
@@ -21,7 +21,7 @@ import {
} from '../constants';
import type { MediaLibraryAction } from '../actions/mediaLibrary';
-import type { Field, MediaFile, MediaLibraryInstance } from '../interface';
+import type { Collection, Field, MediaFile, MediaLibraryInstance } from '../interface';
export interface MediaLibraryDisplayURL {
url?: string;
@@ -39,6 +39,7 @@ export type MediaLibraryState = {
page?: number;
files?: MediaFile[];
config: Record;
+ collection?: Collection;
field?: Field;
value?: string | string[];
replaceIndex?: number;
@@ -75,7 +76,8 @@ function mediaLibrary(
};
case MEDIA_LIBRARY_OPEN: {
- const { controlID, forImage, config, field, value, replaceIndex } = action.payload;
+ const { controlID, forImage, config, collection, field, value, replaceIndex } =
+ action.payload;
const libConfig = config || {};
return {
@@ -85,6 +87,7 @@ function mediaLibrary(
controlID,
canInsert: !!controlID,
config: libConfig,
+ collection,
field,
value,
replaceIndex,
diff --git a/packages/core/src/reducers/selectors/entries.ts b/packages/core/src/reducers/selectors/entries.ts
index e48723d6..cba74d34 100644
--- a/packages/core/src/reducers/selectors/entries.ts
+++ b/packages/core/src/reducers/selectors/entries.ts
@@ -1,20 +1,9 @@
import get from 'lodash/get';
-import groupBy from 'lodash/groupBy';
-import orderBy from 'lodash/orderBy';
-import { SORT_DIRECTION_ASCENDING, SORT_DIRECTION_NONE } from '@staticcms/core/constants';
-import { selectSortDataPath } from '@staticcms/core/lib/util/sort.util';
+import { SORT_DIRECTION_NONE } from '@staticcms/core/constants';
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
-import type {
- Collection,
- Entry,
- Filter,
- Group,
- GroupMap,
- GroupOfEntries,
- Sort,
-} from '@staticcms/core/interface';
+import type { Entry, Group, GroupMap, Sort } from '@staticcms/core/interface';
import type { RootState } from '@staticcms/core/store';
export function selectEntriesSort(entries: RootState, collection: string) {
@@ -22,84 +11,49 @@ export function selectEntriesSort(entries: RootState, collection: string) {
return sort?.[collection];
}
-export function selectEntriesFilter(entries: RootState, collection: string) {
- const filter = entries.entries.filter as Filter | undefined;
- return filter?.[collection] || {};
-}
+export const selectEntriesFilter = (collectionName: string) => (entries: RootState) => {
+ return entries.entries.filter?.[collectionName];
+};
export function selectEntriesGroup(entries: RootState, collection: string) {
const group = entries.entries.group as Group | undefined;
return group?.[collection] || {};
}
-export function selectEntriesGroupField(entries: RootState, collection: string) {
+export const selectEntriesGroupField = (collection: string) => (entries: RootState) => {
const groups = selectEntriesGroup(entries, collection);
- const value = Object.values(groups ?? {}).find(v => v?.active === true);
- return value;
-}
+ return Object.values(groups ?? {}).find(v => v?.active === true);
+};
-export function selectEntriesSortFields(entries: RootState, collection: string) {
- const sort = selectEntriesSort(entries, collection);
- const values = Object.values(sort ?? {}).filter(v => v?.direction !== SORT_DIRECTION_NONE) || [];
-
- return values;
-}
-
-export function selectEntriesFilterFields(entries: RootState, collection: string) {
- const filter = selectEntriesFilter(entries, collection);
- const values = Object.values(filter ?? {}).filter(v => v?.active === true) || [];
- return values;
-}
+export const selectEntriesSortField = (collectionName: string) => (entries: RootState) => {
+ const sort = selectEntriesSort(entries, collectionName);
+ return Object.values(sort ?? {}).find(v => v?.direction !== SORT_DIRECTION_NONE);
+};
export function selectViewStyle(entries: RootState): CollectionViewStyle {
return entries.entries.viewStyle;
}
+export function selectEntriesBySlugs(state: RootState) {
+ return state.entries.entities;
+}
+
export function selectEntry(state: RootState, collection: string, slug: string) {
return state.entries.entities[`${collection}.${slug}`];
}
-export function selectPublishedSlugs(state: RootState, collection: string) {
+export const selectPublishedSlugs = (collection: string) => (state: RootState) => {
return state.entries.pages[collection]?.ids ?? [];
-}
+};
-function getPublishedEntries(state: RootState, collectionName: string) {
- const slugs = selectPublishedSlugs(state, collectionName);
- const entries =
- slugs && (slugs.map(slug => selectEntry(state, collectionName, slug as string)) as Entry[]);
- return entries;
-}
+export const selectPublishedEntries = (collectionName: string) => (state: RootState) => {
+ const slugs = selectPublishedSlugs(collectionName)(state);
+ return (
+ slugs && (slugs.map(slug => selectEntry(state, collectionName, slug as string)) as Entry[])
+ );
+};
-export function selectEntries(state: RootState, collection: Collection) {
- const collectionName = collection.name;
- let entries = getPublishedEntries(state, collectionName);
-
- const sortFields = selectEntriesSortFields(state, collectionName);
- if (sortFields && sortFields.length > 0) {
- const keys = sortFields.map(v => selectSortDataPath(collection, v.key));
- const orders = sortFields.map(v => (v.direction === SORT_DIRECTION_ASCENDING ? 'asc' : 'desc'));
- entries = orderBy(entries, keys, orders);
- }
-
- const filters = selectEntriesFilterFields(state, collectionName);
- if (filters && filters.length > 0) {
- entries = entries.filter(e => {
- const allMatched = filters.every(f => {
- const pattern = f.pattern;
- const field = f.field;
- const data = e!.data || {};
- const toMatch = get(data, field);
- const matched = toMatch !== undefined && new RegExp(String(pattern)).test(String(toMatch));
- return matched;
- });
- return allMatched;
- });
- }
-
- return entries;
-}
-
-function getGroup(entry: Entry, selectedGroup: GroupMap) {
+export function getGroup(entry: Entry, selectedGroup: GroupMap) {
const label = selectedGroup.label;
const field = selectedGroup.field;
@@ -139,35 +93,8 @@ function getGroup(entry: Entry, selectedGroup: GroupMap) {
};
}
-export function selectGroups(state: RootState, collection: Collection) {
- const collectionName = collection.name;
- const entries = getPublishedEntries(state, collectionName);
-
- const selectedGroup = selectEntriesGroupField(state, collectionName);
- if (selectedGroup === undefined) {
- return [];
- }
-
- let groups: Record =
- {};
- const groupedEntries = groupBy(entries, entry => {
- const group = getGroup(entry, selectedGroup);
- groups = { ...groups, [group.id]: group };
- return group.id;
- });
-
- const groupsArray: GroupOfEntries[] = Object.entries(groupedEntries).map(([id, entries]) => {
- return {
- ...groups[id],
- paths: new Set(entries.map(entry => entry.path)),
- };
- });
-
- return groupsArray;
-}
-
export function selectEntryByPath(state: RootState, collection: string, path: string) {
- const slugs = selectPublishedSlugs(state, collection);
+ const slugs = selectPublishedSlugs(collection)(state);
const entries =
slugs && (slugs.map(slug => selectEntry(state, collection, slug as string)) as Entry[]);
diff --git a/packages/core/src/widgets/file/withFileControl.tsx b/packages/core/src/widgets/file/withFileControl.tsx
index 77c7863a..2a596e9a 100644
--- a/packages/core/src/widgets/file/withFileControl.tsx
+++ b/packages/core/src/widgets/file/withFileControl.tsx
@@ -238,7 +238,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
const handleOpenMediaLibrary = useMediaInsert(
internalValue,
- { field, controlID },
+ { collection, field, controlID, forImage },
handleOnChange,
);
@@ -312,10 +312,11 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
replaceIndex: index,
allowMultiple: false,
config,
+ collection,
field,
});
},
- [config, controlID, field, openMediaLibrary, internalValue],
+ [openMediaLibrary, controlID, internalValue, config, collection, field],
);
// TODO Readd when multiple uploads is supported
diff --git a/packages/core/src/widgets/list/ListItem.tsx b/packages/core/src/widgets/list/ListItem.tsx
index d0d670f6..76f74efc 100644
--- a/packages/core/src/widgets/list/ListItem.tsx
+++ b/packages/core/src/widgets/list/ListItem.tsx
@@ -2,7 +2,7 @@ import { styled } from '@mui/material/styles';
import partial from 'lodash/partial';
import React, { useCallback, useMemo, useState } from 'react';
-import EditorControl from '@staticcms/core/components/Editor/EditorControlPane/EditorControl';
+import EditorControl from '@staticcms/core/components/editor/EditorControlPane/EditorControl';
import ListItemTopBar from '@staticcms/core/components/UI/ListItemTopBar';
import Outline from '@staticcms/core/components/UI/Outline';
import { colors } from '@staticcms/core/components/UI/styles';
diff --git a/packages/core/src/widgets/list/__tests__/ListControl.spec.tsx b/packages/core/src/widgets/list/__tests__/ListControl.spec.tsx
index 0541d2e4..9dc39f3b 100644
--- a/packages/core/src/widgets/list/__tests__/ListControl.spec.tsx
+++ b/packages/core/src/widgets/list/__tests__/ListControl.spec.tsx
@@ -52,7 +52,7 @@ const ListControlWrapper = createControlWrapper({
path: 'list',
});
-jest.mock('@staticcms/core/components/Editor/EditorControlPane/EditorControl', () => {
+jest.mock('@staticcms/core/components/editor/EditorControlPane/EditorControl', () => {
return jest.fn(props => {
const { parentPath, field, value } = props;
return (
diff --git a/packages/core/src/widgets/markdown/MarkdownPreview.tsx b/packages/core/src/widgets/markdown/MarkdownPreview.tsx
index 08de9525..c0385f03 100644
--- a/packages/core/src/widgets/markdown/MarkdownPreview.tsx
+++ b/packages/core/src/widgets/markdown/MarkdownPreview.tsx
@@ -7,6 +7,7 @@ import { getShortcodes } from '../../lib/registry';
import withShortcodeMdxComponent from './mdx/withShortcodeMdxComponent';
import useMdx from './plate/hooks/useMdx';
import { processShortcodeConfigToMdx } from './plate/serialization/slate/processShortcodeConfig';
+import { withMdxImage } from '@staticcms/core/components/common/image/Image';
import type { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
import type { FC } from 'react';
@@ -26,13 +27,14 @@ function FallbackComponent({ error }: FallbackComponentProps) {
}
const MarkdownPreview: FC> = previewProps => {
- const { value } = previewProps;
+ const { value, collection, field } = previewProps;
const components = useMemo(
() => ({
Shortcode: withShortcodeMdxComponent({ previewProps }),
+ img: withMdxImage({ collection, field }),
}),
- [previewProps],
+ [collection, field, previewProps],
);
const [state, setValue] = useMdx(value ?? '');
diff --git a/packages/core/src/widgets/markdown/plate/components/common/MediaPopover.tsx b/packages/core/src/widgets/markdown/plate/components/common/MediaPopover.tsx
index 49ba764e..7748cb86 100644
--- a/packages/core/src/widgets/markdown/plate/components/common/MediaPopover.tsx
+++ b/packages/core/src/widgets/markdown/plate/components/common/MediaPopover.tsx
@@ -5,11 +5,10 @@ import Popper from '@mui/material/Popper';
import { styled, useTheme } from '@mui/material/styles';
import TextField from '@mui/material/TextField';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { useFocused } from 'slate-react';
-import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import useIsMediaAsset from '@staticcms/core/lib/hooks/useIsMediaAsset';
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
+import { useWindowEvent } from '@staticcms/core/lib/util/window.util';
import type { Collection, Entry, FileOrImageField, MarkdownField } from '@staticcms/core/interface';
import type { ChangeEvent, KeyboardEvent } from 'react';
@@ -65,7 +64,6 @@ export interface MediaPopoverProps {
onUrlChange: (newValue: string) => void;
onTextChange?: (newValue: string) => void;
onClose: (shouldFocus: boolean) => void;
- mediaOpen?: boolean;
onMediaToggle?: (open: boolean) => void;
onMediaChange: (newValue: string) => void;
onRemove?: () => void;
@@ -87,7 +85,6 @@ const MediaPopover = ({
onUrlChange,
onTextChange,
onClose,
- mediaOpen,
onMediaToggle,
onMediaChange,
onRemove,
@@ -101,9 +98,9 @@ const MediaPopover = ({
const [editing, setEditing] = useState(inserting);
- const hasEditorFocus = useFocused();
- const [hasFocus, setHasFocus] = useState(false);
- const debouncedHasFocus = useDebounce(hasFocus, 150);
+ useWindowEvent('mediaLibraryClose', () => {
+ onMediaToggle?.(false);
+ });
const handleClose = useCallback(
(shouldFocus: boolean) => {
@@ -155,60 +152,11 @@ const MediaPopover = ({
}
}, [anchorEl, editing, inserting, urlDisabled]);
- const [
- { prevAnchorEl, prevHasEditorFocus, prevHasFocus, prevDebouncedHasFocus },
- setPrevFocusState,
- ] = useState<{
- prevAnchorEl: HTMLElement | null;
- prevHasEditorFocus: boolean;
- prevHasFocus: boolean;
- prevDebouncedHasFocus: boolean;
- }>({
- prevAnchorEl: anchorEl,
- prevHasEditorFocus: hasEditorFocus,
- prevHasFocus: hasFocus,
- prevDebouncedHasFocus: debouncedHasFocus,
- });
-
- useEffect(() => {
- if (mediaOpen) {
- return;
- }
-
- if (anchorEl && !prevHasEditorFocus && hasEditorFocus) {
- handleClose(false);
- }
-
- if (anchorEl && (prevHasFocus || prevDebouncedHasFocus) && !hasFocus && !debouncedHasFocus) {
- handleClose(false);
- }
-
- setPrevFocusState({
- prevAnchorEl: anchorEl,
- prevHasEditorFocus: hasEditorFocus,
- prevHasFocus: hasFocus,
- prevDebouncedHasFocus: debouncedHasFocus,
- });
- }, [
- anchorEl,
- debouncedHasFocus,
- handleClose,
- hasEditorFocus,
- hasFocus,
- mediaOpen,
- prevAnchorEl,
- prevDebouncedHasFocus,
- prevHasEditorFocus,
- prevHasFocus,
- ]);
-
const handleFocus = useCallback(() => {
- setHasFocus(true);
onFocus?.();
}, [onFocus]);
const handleBlur = useCallback(() => {
- setHasFocus(false);
onBlur?.();
}, [onBlur]);
@@ -220,7 +168,11 @@ const MediaPopover = ({
[onMediaChange, onMediaToggle],
);
- const handleOpenMediaLibrary = useMediaInsert(url, { field, forImage }, handleMediaChange);
+ const handleOpenMediaLibrary = useMediaInsert(
+ url,
+ { collection, field, forImage },
+ handleMediaChange,
+ );
const handleMediaOpen = useCallback(() => {
onMediaToggle?.(true);
diff --git a/packages/core/src/widgets/markdown/plate/components/nodes/image/withImageElement.tsx b/packages/core/src/widgets/markdown/plate/components/nodes/image/withImageElement.tsx
index 77e9b02d..e7af3da8 100644
--- a/packages/core/src/widgets/markdown/plate/components/nodes/image/withImageElement.tsx
+++ b/packages/core/src/widgets/markdown/plate/components/nodes/image/withImageElement.tsx
@@ -12,6 +12,7 @@ import { useFocused } from 'slate-react';
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
import { isEmpty } from '@staticcms/core/lib/util/string.util';
import { MediaPopover } from '@staticcms/markdown';
+import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
import type { MdImageElement, MdValue } from '@staticcms/markdown';
@@ -35,13 +36,32 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
const { url, alt } = element;
const [internalUrl, setInternalUrl] = useState(url);
const [internalAlt, setInternalAlt] = useState(alt);
+ const [popoverHasFocus, setPopoverHasFocus] = useState(false);
+ const debouncedPopoverHasFocus = useDebounce(popoverHasFocus, 100);
+
+ const [mediaOpen, setMediaOpen] = useState(false);
const imageRef = useRef(null);
const [anchorEl, setAnchorEl] = useState(null);
const hasEditorFocus = useFocused();
+ const debouncedHasEditorFocus = useDebounce(hasEditorFocus, 100);
const handleBlur = useCallback(() => {
- setAnchorEl(null);
+ if (!popoverHasFocus && !mediaOpen) {
+ setAnchorEl(null);
+ }
+ }, [mediaOpen, popoverHasFocus]);
+
+ const handlePopoverFocus = useCallback(() => {
+ setPopoverHasFocus(true);
+ }, []);
+
+ const handlePopoverBlur = useCallback(() => {
+ setPopoverHasFocus(false);
+ }, []);
+
+ const handleMediaToggle = useCallback(() => {
+ setMediaOpen(oldMediaOpen => !oldMediaOpen);
}, []);
const handleChange = useCallback(
@@ -96,7 +116,28 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
const selection = usePlateSelection();
useEffect(() => {
- if (!hasEditorFocus || !selection) {
+ if (
+ hasEditorFocus ||
+ debouncedHasEditorFocus ||
+ mediaOpen ||
+ popoverHasFocus ||
+ debouncedPopoverHasFocus
+ ) {
+ return;
+ }
+
+ handleClose();
+ }, [
+ debouncedHasEditorFocus,
+ debouncedPopoverHasFocus,
+ handleClose,
+ hasEditorFocus,
+ mediaOpen,
+ popoverHasFocus,
+ ]);
+
+ useEffect(() => {
+ if (!hasEditorFocus || !selection || mediaOpen || popoverHasFocus) {
return;
}
@@ -109,12 +150,24 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
}
if (node !== element && node !== firstChild) {
- handleClose();
+ if (anchorEl) {
+ handleClose();
+ }
return;
}
handleOpenPopover();
- }, [handleClose, hasEditorFocus, element, selection, editor, handleOpenPopover]);
+ }, [
+ handleClose,
+ hasEditorFocus,
+ element,
+ selection,
+ editor,
+ handleOpenPopover,
+ mediaOpen,
+ popoverHasFocus,
+ anchorEl,
+ ]);
return (
@@ -140,6 +193,9 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
onMediaChange={handleMediaChange}
onRemove={handleRemove}
forImage
+ onFocus={handlePopoverFocus}
+ onBlur={handlePopoverBlur}
+ onMediaToggle={handleMediaToggle}
/>
{children}
diff --git a/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx b/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx
index 1e1389a2..268e0eea 100644
--- a/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx
+++ b/packages/core/src/widgets/markdown/plate/components/nodes/link/withLinkElement.tsx
@@ -2,17 +2,21 @@ import {
findNodePath,
focusEditor,
getEditorString,
+ getNode,
+ replaceNodeChildren,
setNodes,
unwrapLink,
- upsertLink,
+ usePlateSelection,
} from '@udecode/plate';
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useRef, useState } from 'react';
+import { useFocused } from 'slate-react';
+import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
import MediaPopover from '../../common/MediaPopover';
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
import type { MdLinkElement, MdValue } from '@staticcms/markdown';
-import type { PlateRenderElementProps } from '@udecode/plate';
+import type { PlateRenderElementProps, TText } from '@udecode/plate';
import type { FC, MouseEvent } from 'react';
export interface WithLinkElementProps {
@@ -24,19 +28,49 @@ export interface WithLinkElementProps {
const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkElementProps) => {
const LinkElement: FC> = ({
- attributes,
+ attributes: { ref: _ref, ...attributes },
children,
nodeProps,
element,
editor,
}) => {
- const [anchorEl, setAnchorEl] = useState(null);
+ const urlRef = useRef(null);
const { url } = element;
const path = findNodePath(editor, element);
const [internalUrl, setInternalUrl] = useState(url);
const [internalText, setInternalText] = useState(getEditorString(editor, path));
+ const [popoverHasFocus, setPopoverHasFocus] = useState(false);
+ const debouncedPopoverHasFocus = useDebounce(popoverHasFocus, 100);
+
+ const [mediaOpen, setMediaOpen] = useState(false);
+
+ const [anchorEl, setAnchorEl] = useState(null);
+ const hasEditorFocus = useFocused();
+ const debouncedHasEditorFocus = useDebounce(hasEditorFocus, 100);
+
+ const handleOpenPopover = useCallback(() => {
+ setAnchorEl(urlRef.current);
+ }, []);
+
+ const handleBlur = useCallback(() => {
+ if (!popoverHasFocus && !mediaOpen) {
+ setAnchorEl(null);
+ }
+ }, [mediaOpen, popoverHasFocus]);
+
+ const handlePopoverFocus = useCallback(() => {
+ setPopoverHasFocus(true);
+ }, []);
+
+ const handlePopoverBlur = useCallback(() => {
+ setPopoverHasFocus(false);
+ }, []);
+
+ const handleMediaToggle = useCallback(() => {
+ setMediaOpen(oldMediaOpen => !oldMediaOpen);
+ }, []);
const handleClick = useCallback((event: MouseEvent) => {
setAnchorEl(event.currentTarget);
@@ -50,11 +84,29 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
focusEditor(editor, editor.selection);
}, [editor]);
+ const selection = usePlateSelection();
+
const handleChange = useCallback(
(newUrl: string, newText: string) => {
const path = findNodePath(editor, element);
- path && setNodes(editor, { url: newUrl }, { at: path });
- upsertLink(editor, { url: newUrl, text: newText });
+
+ if (path) {
+ setNodes(
+ editor,
+ { ...element, url: newUrl, children: [{ text: newText }] },
+ { at: path },
+ );
+
+ if (newText?.length && newText !== getEditorString(editor, path)) {
+ replaceNodeChildren(editor, {
+ at: path,
+ nodes: { text: newText },
+ insertOptions: {
+ select: true,
+ },
+ });
+ }
+ }
},
[editor, element],
);
@@ -72,9 +124,83 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
handleChange(internalUrl, internalText);
}, [handleChange, internalText, internalUrl]);
+ useEffect(() => {
+ if (
+ hasEditorFocus ||
+ debouncedHasEditorFocus ||
+ mediaOpen ||
+ popoverHasFocus ||
+ debouncedPopoverHasFocus
+ ) {
+ return;
+ }
+
+ handleClose();
+ }, [
+ debouncedHasEditorFocus,
+ debouncedPopoverHasFocus,
+ handleClose,
+ hasEditorFocus,
+ mediaOpen,
+ popoverHasFocus,
+ ]);
+ useEffect(() => {
+ if (
+ hasEditorFocus ||
+ debouncedHasEditorFocus ||
+ mediaOpen ||
+ popoverHasFocus ||
+ debouncedPopoverHasFocus
+ ) {
+ return;
+ }
+
+ handleClose();
+ }, [
+ debouncedHasEditorFocus,
+ debouncedPopoverHasFocus,
+ handleClose,
+ hasEditorFocus,
+ mediaOpen,
+ popoverHasFocus,
+ ]);
+
+ useEffect(() => {
+ if (!hasEditorFocus || !selection || mediaOpen || popoverHasFocus) {
+ return;
+ }
+
+ const node = getNode(editor, selection.anchor.path);
+ const firstChild =
+ 'children' in element && element.children.length > 0 ? element.children[0] : undefined;
+
+ if (!node) {
+ return;
+ }
+
+ if (node !== element && node !== firstChild) {
+ if (anchorEl) {
+ handleClose();
+ }
+ return;
+ }
+
+ handleOpenPopover();
+ }, [
+ handleClose,
+ hasEditorFocus,
+ element,
+ selection,
+ editor,
+ handleOpenPopover,
+ mediaOpen,
+ popoverHasFocus,
+ anchorEl,
+ ]);
+
return (
- <>
-
+
+
{children}
- >
+
);
};
diff --git a/packages/core/src/widgets/object/ObjectControl.tsx b/packages/core/src/widgets/object/ObjectControl.tsx
index 8959106d..d3c5fd4c 100644
--- a/packages/core/src/widgets/object/ObjectControl.tsx
+++ b/packages/core/src/widgets/object/ObjectControl.tsx
@@ -1,7 +1,7 @@
import { styled } from '@mui/material/styles';
import React, { useCallback, useMemo, useState } from 'react';
-import EditorControl from '@staticcms/core/components/Editor/EditorControlPane/EditorControl';
+import EditorControl from '@staticcms/core/components/editor/EditorControlPane/EditorControl';
import ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
import Outline from '@staticcms/core/components/UI/Outline';
import { transientOptions } from '@staticcms/core/lib';
diff --git a/packages/core/src/widgets/object/__tests__/ObjectControl.spec.tsx b/packages/core/src/widgets/object/__tests__/ObjectControl.spec.tsx
index 9a8ace04..ca56807d 100644
--- a/packages/core/src/widgets/object/__tests__/ObjectControl.spec.tsx
+++ b/packages/core/src/widgets/object/__tests__/ObjectControl.spec.tsx
@@ -35,7 +35,7 @@ const ObjectControlWrapper = createControlWrapper({
path: 'object',
});
-jest.mock('@staticcms/core/components/Editor/EditorControlPane/EditorControl', () => {
+jest.mock('@staticcms/core/components/editor/EditorControlPane/EditorControl', () => {
return jest.fn(props => {
const { parentPath, fieldName, field } = props;
return (