fix: folder collection path (#549)
This commit is contained in:
parent
93915dac35
commit
8f7237ab7c
1
BREAKING_CHANGES.md
Normal file
1
BREAKING_CHANGES.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
- useMediaInsert now requires collection to be passed
|
@ -1,9 +1,9 @@
|
|||||||
{
|
{
|
||||||
"front_limit": 5,
|
"front_limit": 6,
|
||||||
"site_title": "Test",
|
"site_title": "Test",
|
||||||
"posts": {
|
"posts": {
|
||||||
"front_limit": 4,
|
"front_limit": 4,
|
||||||
"author": "Bob",
|
"author": "Bob",
|
||||||
"thumb": "/backends/proxy/assets/upload/kanefreeman_2.jpg"
|
"thumb": "/backends/proxy/assets/upload/kanefreeman_2.jpg"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -2,7 +2,7 @@
|
|||||||
title: Something something something...
|
title: Something something something...
|
||||||
draft: false
|
draft: false
|
||||||
date: 2022-11-01 06:30
|
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
|
# Welcome
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
@ -1,13 +0,0 @@
|
|||||||
---
|
|
||||||
title: Test
|
|
||||||
draft: false
|
|
||||||
date: 2022-11-01 14:28
|
|
||||||
image: /backends/proxy/assets/posts/kittens.jpg
|
|
||||||
---
|
|
||||||
Test2
|
|
||||||
|
|
||||||
<br>
|
|
||||||
![moby-dick.jpg](/assets/upload/moby-dick.jpg)
|
|
||||||
![moby-dick.jpg](/assets/upload/moby-dick.jpg)
|
|
||||||
|
|
||||||
![kanefreeman_2.jpg](/assets/upload/kanefreeman_2.jpg)
|
|
@ -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)
|
Binary file not shown.
After Width: | Height: | Size: 330 KiB |
Binary file not shown.
After Width: | Height: | Size: 310 KiB |
@ -1,7 +0,0 @@
|
|||||||
---
|
|
||||||
title: Test3
|
|
||||||
draft: false
|
|
||||||
date: 2022-11-02 08:43
|
|
||||||
image: /backends/proxy/assets/upload/kanefreeman_2.jpg
|
|
||||||
---
|
|
||||||
test25555
|
|
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
title: Test3
|
||||||
|
draft: false
|
||||||
|
date: 2022-11-02 08:43
|
||||||
|
image: ori_3587884_d966kldqzc6mvdeq67hyk16rnbe3gb1k8eeoy31s_shark-icon.jpg
|
||||||
|
---
|
||||||
|
test2555556
|
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
Binary file not shown.
Before Width: | Height: | Size: 182 KiB |
@ -24,14 +24,15 @@ collections:
|
|||||||
- name: posts
|
- name: posts
|
||||||
label: Posts
|
label: Posts
|
||||||
label_singular: Post
|
label_singular: Post
|
||||||
media_folder: /packages/core/dev-test/backends/proxy/assets/posts
|
|
||||||
public_folder: /backends/proxy/assets/posts
|
|
||||||
description: >
|
description: >
|
||||||
The description is a great place for tone setting, high level information,
|
The description is a great place for tone setting, high level information,
|
||||||
and editing guidelines that are specific to a collection.
|
and editing guidelines that are specific to a collection.
|
||||||
folder: packages/core/dev-test/backends/proxy/_posts
|
folder: packages/core/dev-test/backends/proxy/_posts
|
||||||
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
|
||||||
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
|
summary: '{{title}} -- {{year}}/{{month}}/{{day}}'
|
||||||
|
slug: '{{year}}-{{month}}-{{day}}-{{slug}}'
|
||||||
|
media_folder: ""
|
||||||
|
public_folder: ""
|
||||||
|
path: "{{year}}-{{month}}-{{day}}-{{slug}}/index"
|
||||||
sortable_fields:
|
sortable_fields:
|
||||||
fields:
|
fields:
|
||||||
- title
|
- title
|
||||||
|
@ -44,7 +44,7 @@ import { Cursor } from '../lib/util';
|
|||||||
import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
|
import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
|
||||||
import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors';
|
import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors';
|
||||||
import {
|
import {
|
||||||
selectEntriesSortFields,
|
selectEntriesSortField,
|
||||||
selectIsFetching,
|
selectIsFetching,
|
||||||
selectPublishedSlugs,
|
selectPublishedSlugs,
|
||||||
} from '../reducers/selectors/entries';
|
} from '../reducers/selectors/entries';
|
||||||
@ -653,10 +653,9 @@ export function loadEntries(collection: Collection, page = 0) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const sortFields = selectEntriesSortFields(state, collection.name);
|
const sortField = selectEntriesSortField(collection.name)(state);
|
||||||
if (sortFields && sortFields.length > 0) {
|
if (sortField) {
|
||||||
const field = sortFields[0];
|
return dispatch(sortByField(collection, sortField.key, sortField.direction));
|
||||||
return dispatch(sortByField(collection, field.key, field.direction));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const configState = state.config;
|
const configState = state.config;
|
||||||
@ -956,7 +955,7 @@ export function persistEntry(collection: Collection) {
|
|||||||
const state = getState();
|
const state = getState();
|
||||||
const entryDraft = state.entryDraft;
|
const entryDraft = state.entryDraft;
|
||||||
const fieldsErrors = entryDraft.fieldsErrors;
|
const fieldsErrors = entryDraft.fieldsErrors;
|
||||||
const usedSlugs = selectPublishedSlugs(state, collection.name);
|
const usedSlugs = selectPublishedSlugs(collection.name)(state);
|
||||||
|
|
||||||
// Early return if draft contains validation errors
|
// Early return if draft contains validation errors
|
||||||
if (Object.keys(fieldsErrors).length > 0) {
|
if (Object.keys(fieldsErrors).length > 0) {
|
||||||
|
@ -6,11 +6,9 @@ import {
|
|||||||
LOAD_ASSET_SUCCESS,
|
LOAD_ASSET_SUCCESS,
|
||||||
REMOVE_ASSET,
|
REMOVE_ASSET,
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import { isAbsolutePath } from '../lib/util';
|
|
||||||
import { selectMediaFilePath } from '../lib/util/media.util';
|
import { selectMediaFilePath } from '../lib/util/media.util';
|
||||||
import { selectMediaFileByPath } from '../reducers/selectors/mediaLibrary';
|
|
||||||
import { createAssetProxy } from '../valueObjects/AssetProxy';
|
import { createAssetProxy } from '../valueObjects/AssetProxy';
|
||||||
import { getMediaDisplayURL, getMediaFile, waitForMediaLibraryToLoad } from './mediaLibrary';
|
import { getMediaFile, waitForMediaLibraryToLoad } from './mediaLibrary';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { ThunkDispatch } from 'redux-thunk';
|
import type { ThunkDispatch } from 'redux-thunk';
|
||||||
@ -58,18 +56,9 @@ async function loadAsset(
|
|||||||
dispatch(loadAssetRequest(resolvedPath));
|
dispatch(loadAssetRequest(resolvedPath));
|
||||||
// load asset url from backend
|
// load asset url from backend
|
||||||
await waitForMediaLibraryToLoad(dispatch, getState());
|
await waitForMediaLibraryToLoad(dispatch, getState());
|
||||||
const file = selectMediaFileByPath(getState(), resolvedPath);
|
const { url } = await getMediaFile(getState(), resolvedPath);
|
||||||
|
const asset = createAssetProxy({ path: resolvedPath, url });
|
||||||
let asset: AssetProxy;
|
dispatch(addAsset(asset));
|
||||||
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));
|
|
||||||
}
|
|
||||||
dispatch(loadAssetSuccess(resolvedPath));
|
dispatch(loadAssetSuccess(resolvedPath));
|
||||||
return asset;
|
return asset;
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@ -105,7 +94,8 @@ export function getAsset<F extends BaseField = UnknownField>(
|
|||||||
path,
|
path,
|
||||||
field as Field,
|
field as Field,
|
||||||
);
|
);
|
||||||
let { asset, isLoading, error } = state.medias[resolvedPath] || {};
|
|
||||||
|
const { asset, isLoading } = state.medias[resolvedPath] || {};
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return promiseCache[resolvedPath];
|
return promiseCache[resolvedPath];
|
||||||
}
|
}
|
||||||
@ -116,23 +106,9 @@ export function getAsset<F extends BaseField = UnknownField>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const p = new Promise<AssetProxy>(resolve => {
|
const p = new Promise<AssetProxy>(resolve => {
|
||||||
if (isAbsolutePath(resolvedPath)) {
|
loadAsset(resolvedPath, dispatch, getState).then(asset => {
|
||||||
// asset path is a public url so we can just use it as is
|
|
||||||
asset = createAssetProxy({ path: resolvedPath, url: path });
|
|
||||||
dispatch(addAsset(asset));
|
|
||||||
resolve(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;
|
promiseCache[resolvedPath] = p;
|
||||||
|
@ -34,6 +34,7 @@ import type { AnyAction } from 'redux';
|
|||||||
import type { ThunkDispatch } from 'redux-thunk';
|
import type { ThunkDispatch } from 'redux-thunk';
|
||||||
import type {
|
import type {
|
||||||
BaseField,
|
BaseField,
|
||||||
|
Collection,
|
||||||
DisplayURLState,
|
DisplayURLState,
|
||||||
Field,
|
Field,
|
||||||
ImplementationMediaFile,
|
ImplementationMediaFile,
|
||||||
@ -83,16 +84,28 @@ export function openMediaLibrary<F extends BaseField = UnknownField>(
|
|||||||
allowMultiple?: boolean;
|
allowMultiple?: boolean;
|
||||||
replaceIndex?: number;
|
replaceIndex?: number;
|
||||||
config?: Record<string, unknown>;
|
config?: Record<string, unknown>;
|
||||||
|
collection?: Collection<F>;
|
||||||
field?: F;
|
field?: F;
|
||||||
} = {},
|
} = {},
|
||||||
) {
|
) {
|
||||||
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
|
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
|
||||||
const state = getState();
|
const state = getState();
|
||||||
const mediaLibrary = state.mediaLibrary.externalLibrary;
|
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) {
|
if (mediaLibrary) {
|
||||||
mediaLibrary.show({ id: controlID, value, config, allowMultiple, imagesOnly: forImage });
|
mediaLibrary.show({ id: controlID, value, config, allowMultiple, imagesOnly: forImage });
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
mediaLibraryOpened({
|
mediaLibraryOpened({
|
||||||
controlID,
|
controlID,
|
||||||
@ -101,6 +114,7 @@ export function openMediaLibrary<F extends BaseField = UnknownField>(
|
|||||||
allowMultiple,
|
allowMultiple,
|
||||||
replaceIndex,
|
replaceIndex,
|
||||||
config,
|
config,
|
||||||
|
collection: collection as Collection,
|
||||||
field: field as Field,
|
field: field as Field,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -406,6 +420,7 @@ function mediaLibraryOpened(payload: {
|
|||||||
replaceIndex?: number;
|
replaceIndex?: number;
|
||||||
allowMultiple?: boolean;
|
allowMultiple?: boolean;
|
||||||
config?: Record<string, unknown>;
|
config?: Record<string, unknown>;
|
||||||
|
collection?: Collection;
|
||||||
field?: Field;
|
field?: Field;
|
||||||
}) {
|
}) {
|
||||||
return { type: MEDIA_LIBRARY_OPEN, payload } as const;
|
return { type: MEDIA_LIBRARY_OPEN, payload } as const;
|
||||||
|
@ -14,8 +14,8 @@ 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 { history } from '@staticcms/core/routing/history';
|
||||||
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';
|
||||||
import MediaLibrary from '../MediaLibrary/MediaLibrary';
|
import MediaLibrary from '../MediaLibrary/MediaLibrary';
|
||||||
import Page from '../page/Page';
|
import Page from '../page/Page';
|
||||||
import Snackbars from '../snackbar/Snackbars';
|
import Snackbars from '../snackbar/Snackbars';
|
||||||
@ -183,7 +183,7 @@ const App = ({
|
|||||||
element={<EditorRoute collections={collections} newRecord />}
|
element={<EditorRoute collections={collections} newRecord />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
path="/collections/:name/entries/:slug"
|
path="/collections/:name/entries/*"
|
||||||
element={<EditorRoute collections={collections} />}
|
element={<EditorRoute collections={collections} />}
|
||||||
/>
|
/>
|
||||||
<Route
|
<Route
|
||||||
|
@ -63,6 +63,7 @@ const Entries = ({
|
|||||||
<>
|
<>
|
||||||
{'collection' in otherProps ? (
|
{'collection' in otherProps ? (
|
||||||
<EntryListing
|
<EntryListing
|
||||||
|
key="collection-listing"
|
||||||
collection={otherProps.collection}
|
collection={otherProps.collection}
|
||||||
entries={entries}
|
entries={entries}
|
||||||
viewStyle={viewStyle}
|
viewStyle={viewStyle}
|
||||||
@ -72,6 +73,7 @@ const Entries = ({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EntryListing
|
<EntryListing
|
||||||
|
key="search-listing"
|
||||||
collections={otherProps.collections}
|
collections={otherProps.collections}
|
||||||
entries={entries}
|
entries={entries}
|
||||||
viewStyle={viewStyle}
|
viewStyle={viewStyle}
|
||||||
|
@ -1,22 +1,17 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import React, { useCallback, useEffect, 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 {
|
import { loadEntries, traverseCollectionCursor } from '@staticcms/core/actions/entries';
|
||||||
loadEntries as loadEntriesAction,
|
|
||||||
traverseCollectionCursor as traverseCollectionCursorAction,
|
|
||||||
} from '@staticcms/core/actions/entries';
|
|
||||||
import { colors } from '@staticcms/core/components/UI/styles';
|
import { colors } from '@staticcms/core/components/UI/styles';
|
||||||
|
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||||
|
import useGroups from '@staticcms/core/lib/hooks/useGroups';
|
||||||
import { Cursor } from '@staticcms/core/lib/util';
|
import { Cursor } from '@staticcms/core/lib/util';
|
||||||
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
|
import { selectCollectionEntriesCursor } from '@staticcms/core/reducers/selectors/cursors';
|
||||||
import {
|
import { selectEntriesLoaded, selectIsFetching } from '@staticcms/core/reducers/selectors/entries';
|
||||||
selectEntries,
|
|
||||||
selectEntriesLoaded,
|
|
||||||
selectGroups,
|
|
||||||
selectIsFetching,
|
|
||||||
} from '@staticcms/core/reducers/selectors/entries';
|
|
||||||
import Entries from './Entries';
|
import Entries from './Entries';
|
||||||
|
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
||||||
|
|
||||||
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
||||||
import type { Collection, Entry, GroupOfEntries, TranslatedProps } from '@staticcms/core/interface';
|
import type { Collection, Entry, GroupOfEntries, TranslatedProps } from '@staticcms/core/interface';
|
||||||
@ -48,90 +43,6 @@ function getGroupTitle(group: GroupOfEntries, t: t) {
|
|||||||
return `${label} ${value}`.trim();
|
return `${label} ${value}`.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
function withGroups(
|
|
||||||
groups: GroupOfEntries[],
|
|
||||||
entries: Entry[],
|
|
||||||
EntriesToRender: ComponentType<EntriesToRenderProps>,
|
|
||||||
t: t,
|
|
||||||
) {
|
|
||||||
return groups.map(group => {
|
|
||||||
const title = getGroupTitle(group, t);
|
|
||||||
return (
|
|
||||||
<GroupContainer key={group.id} id={group.id}>
|
|
||||||
<GroupHeading>{title}</GroupHeading>
|
|
||||||
<EntriesToRender entries={getGroupEntries(entries, group.paths)} />
|
|
||||||
</GroupContainer>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EntriesToRenderProps {
|
|
||||||
entries: Entry[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const EntriesCollection = ({
|
|
||||||
collection,
|
|
||||||
entries,
|
|
||||||
groups,
|
|
||||||
isFetching,
|
|
||||||
viewStyle,
|
|
||||||
cursor,
|
|
||||||
page,
|
|
||||||
traverseCollectionCursor,
|
|
||||||
t,
|
|
||||||
entriesLoaded,
|
|
||||||
readyToLoad,
|
|
||||||
loadEntries,
|
|
||||||
}: TranslatedProps<EntriesCollectionProps>) => {
|
|
||||||
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 (
|
|
||||||
<Entries
|
|
||||||
collection={collection}
|
|
||||||
entries={entries}
|
|
||||||
isFetching={isFetching}
|
|
||||||
collectionName={collection.label}
|
|
||||||
viewStyle={viewStyle}
|
|
||||||
cursor={cursor}
|
|
||||||
handleCursorActions={handleCursorActions}
|
|
||||||
page={page}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[collection, cursor, handleCursorActions, isFetching, page, viewStyle],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (groups && groups.length > 0) {
|
|
||||||
return <>{withGroups(groups, entries, EntriesToRender, t)}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <EntriesToRender entries={entries} />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function filterNestedEntries(path: string, collectionFolder: string, entries: Entry[]) {
|
export function filterNestedEntries(path: string, collectionFolder: string, entries: Entry[]) {
|
||||||
const filtered = entries.filter(e => {
|
const filtered = entries.filter(e => {
|
||||||
const entryPath = e.path.slice(collectionFolder.length + 1);
|
const entryPath = e.path.slice(collectionFolder.length + 1);
|
||||||
@ -152,6 +63,94 @@ export function filterNestedEntries(path: string, collectionFolder: string, entr
|
|||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const EntriesCollection = ({
|
||||||
|
collection,
|
||||||
|
filterTerm,
|
||||||
|
isFetching,
|
||||||
|
viewStyle,
|
||||||
|
cursor,
|
||||||
|
page,
|
||||||
|
t,
|
||||||
|
entriesLoaded,
|
||||||
|
readyToLoad,
|
||||||
|
}: TranslatedProps<EntriesCollectionProps>) => {
|
||||||
|
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 (
|
||||||
|
<GroupContainer key={group.id} id={group.id}>
|
||||||
|
<GroupHeading>{title}</GroupHeading>
|
||||||
|
<Entries
|
||||||
|
collection={collection}
|
||||||
|
entries={getGroupEntries(filteredEntries, group.paths)}
|
||||||
|
isFetching={isFetching}
|
||||||
|
collectionName={collection.label}
|
||||||
|
viewStyle={viewStyle}
|
||||||
|
cursor={cursor}
|
||||||
|
handleCursorActions={handleCursorActions}
|
||||||
|
page={page}
|
||||||
|
/>
|
||||||
|
</GroupContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Entries
|
||||||
|
key="entries-without-group"
|
||||||
|
collection={collection}
|
||||||
|
entries={filteredEntries}
|
||||||
|
isFetching={isFetching}
|
||||||
|
collectionName={collection.label}
|
||||||
|
viewStyle={viewStyle}
|
||||||
|
cursor={cursor}
|
||||||
|
handleCursorActions={handleCursorActions}
|
||||||
|
page={page}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface EntriesCollectionOwnProps {
|
interface EntriesCollectionOwnProps {
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
viewStyle: CollectionViewStyle;
|
viewStyle: CollectionViewStyle;
|
||||||
@ -163,27 +162,16 @@ function mapStateToProps(state: RootState, ownProps: EntriesCollectionOwnProps)
|
|||||||
const { collection, viewStyle, filterTerm } = ownProps;
|
const { collection, viewStyle, filterTerm } = ownProps;
|
||||||
const page = state.entries.pages[collection.name]?.page;
|
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 entriesLoaded = selectEntriesLoaded(state, collection.name);
|
||||||
const isFetching = selectIsFetching(state, collection.name);
|
const isFetching = selectIsFetching(state, collection.name);
|
||||||
|
|
||||||
const rawCursor = selectCollectionEntriesCursor(state, collection.name);
|
const rawCursor = selectCollectionEntriesCursor(state, collection.name);
|
||||||
const cursor = Cursor.create(rawCursor).clearData();
|
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 = {
|
const mapDispatchToProps = {};
|
||||||
loadEntries: loadEntriesAction,
|
|
||||||
traverseCollectionCursor: traverseCollectionCursorAction,
|
|
||||||
};
|
|
||||||
|
|
||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
export type EntriesCollectionProps = ConnectedProps<typeof connector>;
|
export type EntriesCollectionProps = ConnectedProps<typeof connector>;
|
||||||
|
@ -4,10 +4,8 @@ import CardContent from '@mui/material/CardContent';
|
|||||||
import CardMedia from '@mui/material/CardMedia';
|
import CardMedia from '@mui/material/CardMedia';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { Link } from 'react-router-dom';
|
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 { VIEW_STYLE_GRID, VIEW_STYLE_LIST } from '@staticcms/core/constants/collectionViews';
|
||||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||||
import { getPreviewCard } from '@staticcms/core/lib/registry';
|
import { getPreviewCard } from '@staticcms/core/lib/registry';
|
||||||
@ -17,28 +15,58 @@ import {
|
|||||||
selectTemplateName,
|
selectTemplateName,
|
||||||
} from '@staticcms/core/lib/util/collection.util';
|
} from '@staticcms/core/lib/util/collection.util';
|
||||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||||
import { selectIsLoadingAsset } from '@staticcms/core/reducers/selectors/medias';
|
|
||||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||||
import useWidgetsFor from '../../common/widget/useWidgetsFor';
|
import useWidgetsFor from '../../common/widget/useWidgetsFor';
|
||||||
|
|
||||||
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
||||||
import type { Collection, Entry, Field } from '@staticcms/core/interface';
|
import type { Collection, Entry, FileOrImageField, MediaField } from '@staticcms/core/interface';
|
||||||
import type { RootState } from '@staticcms/core/store';
|
|
||||||
import type { ConnectedProps } from 'react-redux';
|
export interface EntryCardProps {
|
||||||
|
entry: Entry;
|
||||||
|
imageFieldName?: string | null | undefined;
|
||||||
|
collection: Collection;
|
||||||
|
collectionLabel?: string;
|
||||||
|
viewStyle?: CollectionViewStyle;
|
||||||
|
}
|
||||||
|
|
||||||
const EntryCard = ({
|
const EntryCard = ({
|
||||||
collection,
|
collection,
|
||||||
entry,
|
entry,
|
||||||
path,
|
|
||||||
image,
|
|
||||||
imageField,
|
|
||||||
collectionLabel,
|
collectionLabel,
|
||||||
viewStyle = VIEW_STYLE_LIST,
|
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 summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
||||||
|
|
||||||
const fields = selectFields(collection, entry.slug);
|
const fields = selectFields(collection, entry.slug);
|
||||||
const imageUrl = useMediaAsset(image, collection, imageField, entry);
|
const imageUrl = useMediaAsset(image, collection as Collection<MediaField>, imageField, entry);
|
||||||
|
|
||||||
const config = useAppSelector(selectConfig);
|
const config = useAppSelector(selectConfig);
|
||||||
|
|
||||||
@ -97,51 +125,4 @@ const EntryCard = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
interface EntryCardOwnProps {
|
export default EntryCard;
|
||||||
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<typeof connector>;
|
|
||||||
|
|
||||||
export default connector(EntryCard);
|
|
||||||
|
@ -8,7 +8,7 @@ import { selectFields, selectInferredField } from '@staticcms/core/lib/util/coll
|
|||||||
import EntryCard from './EntryCard';
|
import EntryCard from './EntryCard';
|
||||||
|
|
||||||
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
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';
|
import type Cursor from '@staticcms/core/lib/util/Cursor';
|
||||||
|
|
||||||
interface CardsGridProps {
|
interface CardsGridProps {
|
||||||
@ -100,19 +100,19 @@ const EntryListing = ({
|
|||||||
const renderedCards = useMemo(() => {
|
const renderedCards = useMemo(() => {
|
||||||
if ('collection' in otherProps) {
|
if ('collection' in otherProps) {
|
||||||
const inferredFields = inferFields(otherProps.collection);
|
const inferredFields = inferFields(otherProps.collection);
|
||||||
return entries.map((entry, idx) => (
|
return entries.map(entry => (
|
||||||
<EntryCard
|
<EntryCard
|
||||||
collection={otherProps.collection}
|
collection={otherProps.collection}
|
||||||
inferredFields={inferredFields}
|
imageFieldName={inferredFields.imageField}
|
||||||
viewStyle={viewStyle}
|
viewStyle={viewStyle}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
key={idx}
|
key={entry.slug}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSingleCollectionInList = Object.keys(otherProps.collections).length === 1;
|
const isSingleCollectionInList = Object.keys(otherProps.collections).length === 1;
|
||||||
return entries.map((entry, idx) => {
|
return entries.map(entry => {
|
||||||
const collectionName = entry.collection;
|
const collectionName = entry.collection;
|
||||||
const collection = Object.values(otherProps.collections).find(
|
const collection = Object.values(otherProps.collections).find(
|
||||||
coll => coll.name === collectionName,
|
coll => coll.name === collectionName,
|
||||||
@ -123,9 +123,9 @@ const EntryListing = ({
|
|||||||
<EntryCard
|
<EntryCard
|
||||||
collection={collection}
|
collection={collection}
|
||||||
entry={entry}
|
entry={entry}
|
||||||
inferredFields={inferredFields}
|
imageFieldName={inferredFields.imageField}
|
||||||
collectionLabel={collectionLabel}
|
collectionLabel={collectionLabel}
|
||||||
key={idx}
|
key={entry.slug}
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
});
|
});
|
||||||
|
@ -3,18 +3,15 @@ import { styled } from '@mui/material/styles';
|
|||||||
import sortBy from 'lodash/sortBy';
|
import sortBy from 'lodash/sortBy';
|
||||||
import { dirname, sep } from 'path';
|
import { dirname, sep } from 'path';
|
||||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
|
||||||
import { colors, components } from '@staticcms/core/components/UI/styles';
|
import { colors, components } from '@staticcms/core/components/UI/styles';
|
||||||
import { transientOptions } from '@staticcms/core/lib';
|
import { transientOptions } from '@staticcms/core/lib';
|
||||||
|
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||||
import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util';
|
import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util';
|
||||||
import { stringTemplate } from '@staticcms/core/lib/widgets';
|
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 { Collection, Entry } from '@staticcms/core/interface';
|
||||||
import type { RootState } from '@staticcms/core/store';
|
|
||||||
import type { ConnectedProps } from 'react-redux';
|
|
||||||
|
|
||||||
const { addFileTemplateFields } = stringTemplate;
|
const { addFileTemplateFields } = stringTemplate;
|
||||||
|
|
||||||
@ -271,7 +268,14 @@ export function updateNode(
|
|||||||
return updater([...treeData]);
|
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<TreeNodeData[]>(getTreeData(collection, entries));
|
const [treeData, setTreeData] = useState<TreeNodeData[]>(getTreeData(collection, entries));
|
||||||
const [selected, setSelected] = useState<TreeNodeData | null>(null);
|
const [selected, setSelected] = useState<TreeNodeData | null>(null);
|
||||||
const [useFilter, setUseFilter] = useState(true);
|
const [useFilter, setUseFilter] = useState(true);
|
||||||
@ -337,18 +341,4 @@ const NestedCollection = ({ collection, entries, filterTerm }: NestedCollectionP
|
|||||||
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;
|
return <TreeNode collection={collection} treeData={treeData} onToggle={onToggle} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface NestedCollectionOwnProps {
|
export default NestedCollection;
|
||||||
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<typeof connector>;
|
|
||||||
|
|
||||||
export default connector(NestedCollection);
|
|
||||||
|
@ -12,7 +12,9 @@ interface EditorRouteProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const EditorRoute = ({ newRecord = false, collections }: EditorRouteProps) => {
|
const EditorRoute = ({ newRecord = false, collections }: EditorRouteProps) => {
|
||||||
const { name, slug } = useParams();
|
const { name, ...params } = useParams();
|
||||||
|
const slug = params['*'];
|
||||||
|
|
||||||
const shouldRedirect = useMemo(() => {
|
const shouldRedirect = useMemo(() => {
|
||||||
if (!name) {
|
if (!name) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import fuzzy from 'fuzzy';
|
import fuzzy from 'fuzzy';
|
||||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { translate } from 'react-polyglot';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -12,12 +11,13 @@ import {
|
|||||||
persistMedia as persistMediaAction,
|
persistMedia as persistMediaAction,
|
||||||
} from '@staticcms/core/actions/mediaLibrary';
|
} from '@staticcms/core/actions/mediaLibrary';
|
||||||
import { fileExtension } from '@staticcms/core/lib/util';
|
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 { selectMediaFiles } from '@staticcms/core/reducers/selectors/mediaLibrary';
|
||||||
import alert from '../UI/Alert';
|
import alert from '../UI/Alert';
|
||||||
import confirm from '../UI/Confirm';
|
import confirm from '../UI/Confirm';
|
||||||
import MediaLibraryModal from './MediaLibraryModal';
|
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 { RootState } from '@staticcms/core/store';
|
||||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
@ -53,7 +53,7 @@ const MediaLibrary = ({
|
|||||||
isDeleting,
|
isDeleting,
|
||||||
hasNextPage,
|
hasNextPage,
|
||||||
isPaginating,
|
isPaginating,
|
||||||
config,
|
config: mediaConfig,
|
||||||
loadMedia,
|
loadMedia,
|
||||||
dynamicSearchQuery,
|
dynamicSearchQuery,
|
||||||
page,
|
page,
|
||||||
@ -61,31 +61,28 @@ const MediaLibrary = ({
|
|||||||
deleteMedia,
|
deleteMedia,
|
||||||
insertMedia,
|
insertMedia,
|
||||||
closeMediaLibrary,
|
closeMediaLibrary,
|
||||||
|
collection,
|
||||||
field,
|
field,
|
||||||
t,
|
}: MediaLibraryProps) => {
|
||||||
}: TranslatedProps<MediaLibraryProps>) => {
|
|
||||||
const [selectedFile, setSelectedFile] = useState<MediaFile | null>(null);
|
const [selectedFile, setSelectedFile] = useState<MediaFile | null>(null);
|
||||||
const [query, setQuery] = useState<string | undefined>(undefined);
|
const [query, setQuery] = useState<string | undefined>(undefined);
|
||||||
|
|
||||||
const [prevIsVisible, setPrevIsVisible] = useState(false);
|
const [prevIsVisible, setPrevIsVisible] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadMedia();
|
loadMedia({});
|
||||||
}, [loadMedia]);
|
}, [loadMedia]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!prevIsVisible && isVisible) {
|
if (!prevIsVisible && isVisible) {
|
||||||
setSelectedFile(null);
|
setSelectedFile(null);
|
||||||
setQuery('');
|
setQuery('');
|
||||||
|
loadMedia();
|
||||||
|
} else if (prevIsVisible && !isVisible) {
|
||||||
|
window.dispatchEvent(new MediaLibraryCloseEvent());
|
||||||
}
|
}
|
||||||
|
|
||||||
setPrevIsVisible(isVisible);
|
setPrevIsVisible(isVisible);
|
||||||
}, [isVisible, prevIsVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!prevIsVisible && isVisible) {
|
|
||||||
loadMedia();
|
|
||||||
}
|
|
||||||
}, [isVisible, loadMedia, prevIsVisible]);
|
}, [isVisible, loadMedia, prevIsVisible]);
|
||||||
|
|
||||||
const loadDisplayURL = useCallback(
|
const loadDisplayURL = useCallback(
|
||||||
@ -155,7 +152,7 @@ const MediaLibrary = ({
|
|||||||
[selectedFile?.key],
|
[selectedFile?.key],
|
||||||
);
|
);
|
||||||
|
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>();
|
const scrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollToTop = () => {
|
const scrollToTop = () => {
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
scrollContainerRef.current.scrollTop = 0;
|
scrollContainerRef.current.scrollTop = 0;
|
||||||
@ -189,7 +186,8 @@ const MediaLibrary = ({
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const files = [...Array.from(fileList)];
|
const files = [...Array.from(fileList)];
|
||||||
const file = files[0];
|
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) {
|
if (maxFileSize && file.size > maxFileSize) {
|
||||||
alert({
|
alert({
|
||||||
@ -213,7 +211,7 @@ const MediaLibrary = ({
|
|||||||
event.target.value = '';
|
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.
|
* 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
|
* Because file names don't have spaces, typing a space eliminates all
|
||||||
* potential matches, so we strip them all out internally before running the
|
* potential matches, so we strip them all out internally before running the
|
||||||
@ -318,11 +316,10 @@ const MediaLibrary = ({
|
|||||||
*/
|
*/
|
||||||
const strippedQuery = query.replace(/ /g, '');
|
const strippedQuery = query.replace(/ /g, '');
|
||||||
const matches = fuzzy.filter(strippedQuery, files, { extract: file => file.name });
|
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];
|
const file = files[match.index];
|
||||||
return { ...file, queryIndex };
|
return { ...file, queryIndex };
|
||||||
});
|
}) as MediaFile[];
|
||||||
return matchFiles;
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -339,7 +336,7 @@ const MediaLibrary = ({
|
|||||||
hasNextPage={hasNextPage}
|
hasNextPage={hasNextPage}
|
||||||
isPaginating={isPaginating}
|
isPaginating={isPaginating}
|
||||||
query={query}
|
query={query}
|
||||||
selectedFile={selectedFile}
|
selectedFile={selectedFile ?? undefined}
|
||||||
handleFilter={filterImages}
|
handleFilter={filterImages}
|
||||||
handleQuery={queryFilter}
|
handleQuery={queryFilter}
|
||||||
toTableData={toTableData}
|
toTableData={toTableData}
|
||||||
@ -350,12 +347,13 @@ const MediaLibrary = ({
|
|||||||
handleDelete={handleDelete}
|
handleDelete={handleDelete}
|
||||||
handleInsert={handleInsert}
|
handleInsert={handleInsert}
|
||||||
handleDownload={handleDownload}
|
handleDownload={handleDownload}
|
||||||
setScrollContainerRef={scrollContainerRef}
|
scrollContainerRef={scrollContainerRef}
|
||||||
handleAssetClick={handleAssetClick}
|
handleAssetClick={handleAssetClick}
|
||||||
handleLoadMore={handleLoadMore}
|
handleLoadMore={handleLoadMore}
|
||||||
displayURLs={displayURLs}
|
displayURLs={displayURLs}
|
||||||
loadDisplayURL={loadDisplayURL}
|
loadDisplayURL={loadDisplayURL}
|
||||||
t={t}
|
collection={collection}
|
||||||
|
field={field}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -379,6 +377,7 @@ function mapStateToProps(state: RootState) {
|
|||||||
page: mediaLibrary.page,
|
page: mediaLibrary.page,
|
||||||
hasNextPage: mediaLibrary.hasNextPage,
|
hasNextPage: mediaLibrary.hasNextPage,
|
||||||
isPaginating: mediaLibrary.isPaginating,
|
isPaginating: mediaLibrary.isPaginating,
|
||||||
|
collection: mediaLibrary.collection,
|
||||||
field,
|
field,
|
||||||
};
|
};
|
||||||
return { ...mediaLibraryProps };
|
return { ...mediaLibraryProps };
|
||||||
@ -396,4 +395,4 @@ const mapDispatchToProps = {
|
|||||||
const connector = connect(mapStateToProps, mapDispatchToProps);
|
const connector = connect(mapStateToProps, mapDispatchToProps);
|
||||||
export type MediaLibraryProps = ConnectedProps<typeof connector>;
|
export type MediaLibraryProps = ConnectedProps<typeof connector>;
|
||||||
|
|
||||||
export default connector(translate()(MediaLibrary));
|
export default connector(MediaLibrary);
|
||||||
|
@ -4,8 +4,11 @@ import React, { useEffect } from 'react';
|
|||||||
import { borders, colors, effects, lengths, shadows } from '@staticcms/core/components/UI/styles';
|
import { borders, colors, effects, lengths, shadows } from '@staticcms/core/components/UI/styles';
|
||||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||||
import transientOptions from '@staticcms/core/lib/util/transientOptions';
|
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 { MediaLibraryDisplayURL } from '@staticcms/core/reducers/mediaLibrary';
|
||||||
|
import type { Field, Collection } from '@staticcms/core/interface';
|
||||||
|
|
||||||
const IMAGE_HEIGHT = 160;
|
const IMAGE_HEIGHT = 160;
|
||||||
|
|
||||||
@ -89,6 +92,8 @@ interface MediaLibraryCardProps {
|
|||||||
isViewableImage: boolean;
|
isViewableImage: boolean;
|
||||||
loadDisplayURL: () => void;
|
loadDisplayURL: () => void;
|
||||||
isDraft?: boolean;
|
isDraft?: boolean;
|
||||||
|
collection?: Collection;
|
||||||
|
field?: Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaLibraryCard = ({
|
const MediaLibraryCard = ({
|
||||||
@ -103,9 +108,12 @@ const MediaLibraryCard = ({
|
|||||||
type,
|
type,
|
||||||
isViewableImage,
|
isViewableImage,
|
||||||
isDraft,
|
isDraft,
|
||||||
|
collection,
|
||||||
|
field,
|
||||||
loadDisplayURL,
|
loadDisplayURL,
|
||||||
}: MediaLibraryCardProps) => {
|
}: MediaLibraryCardProps) => {
|
||||||
const url = useMediaAsset(displayURL.url);
|
const entry = useAppSelector(selectEditingDraft);
|
||||||
|
const url = useMediaAsset(displayURL.url, collection, field, entry);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!displayURL.url) {
|
if (!displayURL.url) {
|
||||||
|
@ -6,12 +6,12 @@ import { FixedSizeGrid as Grid } from 'react-window';
|
|||||||
|
|
||||||
import MediaLibraryCard from './MediaLibraryCard';
|
import MediaLibraryCard from './MediaLibraryCard';
|
||||||
|
|
||||||
import type { GridChildComponentProps } from 'react-window';
|
import type { Collection, Field, MediaFile } from '@staticcms/core/interface';
|
||||||
import type { MediaFile } from '@staticcms/core/interface';
|
|
||||||
import type {
|
import type {
|
||||||
MediaLibraryDisplayURL,
|
MediaLibraryDisplayURL,
|
||||||
MediaLibraryState,
|
MediaLibraryState,
|
||||||
} from '@staticcms/core/reducers/mediaLibrary';
|
} from '@staticcms/core/reducers/mediaLibrary';
|
||||||
|
import type { GridChildComponentProps } from 'react-window';
|
||||||
|
|
||||||
export interface MediaLibraryCardItem {
|
export interface MediaLibraryCardItem {
|
||||||
displayURL?: MediaLibraryDisplayURL;
|
displayURL?: MediaLibraryDisplayURL;
|
||||||
@ -25,7 +25,7 @@ export interface MediaLibraryCardItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MediaLibraryCardGridProps {
|
export interface MediaLibraryCardGridProps {
|
||||||
setScrollContainerRef: () => void;
|
scrollContainerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
mediaItems: MediaFile[];
|
mediaItems: MediaFile[];
|
||||||
isSelectedFile: (file: MediaFile) => boolean;
|
isSelectedFile: (file: MediaFile) => boolean;
|
||||||
onAssetClick: (asset: MediaFile) => void;
|
onAssetClick: (asset: MediaFile) => void;
|
||||||
@ -39,6 +39,8 @@ export interface MediaLibraryCardGridProps {
|
|||||||
cardMargin: string;
|
cardMargin: string;
|
||||||
loadDisplayURL: (asset: MediaFile) => void;
|
loadDisplayURL: (asset: MediaFile) => void;
|
||||||
displayURLs: MediaLibraryState['displayURLs'];
|
displayURLs: MediaLibraryState['displayURLs'];
|
||||||
|
collection?: Collection;
|
||||||
|
field?: Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CardGridItemData = MediaLibraryCardGridProps & {
|
export type CardGridItemData = MediaLibraryCardGridProps & {
|
||||||
@ -61,6 +63,8 @@ const CardWrapper = ({
|
|||||||
loadDisplayURL,
|
loadDisplayURL,
|
||||||
columnCount,
|
columnCount,
|
||||||
gutter,
|
gutter,
|
||||||
|
collection,
|
||||||
|
field,
|
||||||
},
|
},
|
||||||
}: GridChildComponentProps<CardGridItemData>) => {
|
}: GridChildComponentProps<CardGridItemData>) => {
|
||||||
const index = rowIndex * columnCount + columnIndex;
|
const index = rowIndex * columnCount + columnIndex;
|
||||||
@ -93,6 +97,8 @@ const CardWrapper = ({
|
|||||||
loadDisplayURL={() => loadDisplayURL(file)}
|
loadDisplayURL={() => loadDisplayURL(file)}
|
||||||
type={file.type}
|
type={file.type}
|
||||||
isViewableImage={file.isViewableImage ?? false}
|
isViewableImage={file.isViewableImage ?? false}
|
||||||
|
collection={collection}
|
||||||
|
field={field}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -126,7 +132,7 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => {
|
|||||||
cardHeight: inputCardHeight,
|
cardHeight: inputCardHeight,
|
||||||
cardMargin,
|
cardMargin,
|
||||||
mediaItems,
|
mediaItems,
|
||||||
setScrollContainerRef,
|
scrollContainerRef,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -141,7 +147,7 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => {
|
|||||||
const rowCount = Math.ceil(mediaItems.length / columnCount);
|
const rowCount = Math.ceil(mediaItems.length / columnCount);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledCardGridContainer $width={width} $height={height} ref={setScrollContainerRef}>
|
<StyledCardGridContainer $width={width} $height={height} ref={scrollContainerRef}>
|
||||||
<Grid
|
<Grid
|
||||||
columnCount={columnCount}
|
columnCount={columnCount}
|
||||||
columnWidth={columnWidth}
|
columnWidth={columnWidth}
|
||||||
@ -168,7 +174,7 @@ const VirtualizedGrid = (props: MediaLibraryCardGridProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PaginatedGrid = ({
|
const PaginatedGrid = ({
|
||||||
setScrollContainerRef,
|
scrollContainerRef,
|
||||||
mediaItems,
|
mediaItems,
|
||||||
isSelectedFile,
|
isSelectedFile,
|
||||||
onAssetClick,
|
onAssetClick,
|
||||||
@ -182,9 +188,11 @@ const PaginatedGrid = ({
|
|||||||
onLoadMore,
|
onLoadMore,
|
||||||
isPaginating,
|
isPaginating,
|
||||||
paginatingMessage,
|
paginatingMessage,
|
||||||
|
collection,
|
||||||
|
field,
|
||||||
}: MediaLibraryCardGridProps) => {
|
}: MediaLibraryCardGridProps) => {
|
||||||
return (
|
return (
|
||||||
<StyledCardGridContainer ref={setScrollContainerRef}>
|
<StyledCardGridContainer ref={scrollContainerRef}>
|
||||||
<CardGrid>
|
<CardGrid>
|
||||||
{mediaItems.map(file => (
|
{mediaItems.map(file => (
|
||||||
<MediaLibraryCard
|
<MediaLibraryCard
|
||||||
@ -201,6 +209,8 @@ const PaginatedGrid = ({
|
|||||||
loadDisplayURL={() => loadDisplayURL(file)}
|
loadDisplayURL={() => loadDisplayURL(file)}
|
||||||
type={file.type}
|
type={file.type}
|
||||||
isViewableImage={file.isViewableImage ?? false}
|
isViewableImage={file.isViewableImage ?? false}
|
||||||
|
collection={collection}
|
||||||
|
field={field}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore} />}
|
{!canLoadMore ? null : <Waypoint onEnter={onLoadMore} />}
|
||||||
|
@ -11,9 +11,9 @@ import EmptyMessage from './EmptyMessage';
|
|||||||
import MediaLibraryCardGrid from './MediaLibraryCardGrid';
|
import MediaLibraryCardGrid from './MediaLibraryCardGrid';
|
||||||
import MediaLibraryTop from './MediaLibraryTop';
|
import MediaLibraryTop from './MediaLibraryTop';
|
||||||
|
|
||||||
import type { ChangeEvent, ChangeEventHandler, KeyboardEventHandler } from 'react';
|
import type { Collection, Field, MediaFile, TranslatedProps } from '@staticcms/core/interface';
|
||||||
import type { MediaFile, TranslatedProps } from '@staticcms/core/interface';
|
|
||||||
import type { MediaLibraryState } from '@staticcms/core/reducers/mediaLibrary';
|
import type { MediaLibraryState } from '@staticcms/core/reducers/mediaLibrary';
|
||||||
|
import type { ChangeEvent, ChangeEventHandler, FC, KeyboardEventHandler } from 'react';
|
||||||
|
|
||||||
const StyledFab = styled(Fab)`
|
const StyledFab = styled(Fab)`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -95,11 +95,13 @@ interface MediaLibraryModalProps {
|
|||||||
handleDelete: () => void;
|
handleDelete: () => void;
|
||||||
handleInsert: () => void;
|
handleInsert: () => void;
|
||||||
handleDownload: () => void;
|
handleDownload: () => void;
|
||||||
setScrollContainerRef: () => void;
|
scrollContainerRef: React.MutableRefObject<HTMLDivElement | null>;
|
||||||
handleAssetClick: (asset: MediaFile) => void;
|
handleAssetClick: (asset: MediaFile) => void;
|
||||||
handleLoadMore: () => void;
|
handleLoadMore: () => void;
|
||||||
loadDisplayURL: (file: MediaFile) => void;
|
loadDisplayURL: (file: MediaFile) => void;
|
||||||
displayURLs: MediaLibraryState['displayURLs'];
|
displayURLs: MediaLibraryState['displayURLs'];
|
||||||
|
collection?: Collection;
|
||||||
|
field?: Field;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MediaLibraryModal = ({
|
const MediaLibraryModal = ({
|
||||||
@ -126,11 +128,13 @@ const MediaLibraryModal = ({
|
|||||||
handleDelete,
|
handleDelete,
|
||||||
handleInsert,
|
handleInsert,
|
||||||
handleDownload,
|
handleDownload,
|
||||||
setScrollContainerRef,
|
scrollContainerRef,
|
||||||
handleAssetClick,
|
handleAssetClick,
|
||||||
handleLoadMore,
|
handleLoadMore,
|
||||||
loadDisplayURL,
|
loadDisplayURL,
|
||||||
displayURLs,
|
displayURLs,
|
||||||
|
collection,
|
||||||
|
field,
|
||||||
t,
|
t,
|
||||||
}: TranslatedProps<MediaLibraryModalProps>) => {
|
}: TranslatedProps<MediaLibraryModalProps>) => {
|
||||||
const filteredFiles = forImage ? handleFilter(files) : files;
|
const filteredFiles = forImage ? handleFilter(files) : files;
|
||||||
@ -177,7 +181,7 @@ const MediaLibraryModal = ({
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
{!shouldShowEmptyMessage ? null : <EmptyMessage content={emptyMessage} />}
|
{!shouldShowEmptyMessage ? null : <EmptyMessage content={emptyMessage} />}
|
||||||
<MediaLibraryCardGrid
|
<MediaLibraryCardGrid
|
||||||
setScrollContainerRef={setScrollContainerRef}
|
scrollContainerRef={scrollContainerRef}
|
||||||
mediaItems={tableData}
|
mediaItems={tableData}
|
||||||
isSelectedFile={file => selectedFile?.key === file.key}
|
isSelectedFile={file => selectedFile?.key === file.key}
|
||||||
onAssetClick={handleAssetClick}
|
onAssetClick={handleAssetClick}
|
||||||
@ -191,10 +195,12 @@ const MediaLibraryModal = ({
|
|||||||
cardMargin={cardMargin}
|
cardMargin={cardMargin}
|
||||||
loadDisplayURL={loadDisplayURL}
|
loadDisplayURL={loadDisplayURL}
|
||||||
displayURLs={displayURLs}
|
displayURLs={displayURLs}
|
||||||
|
collection={collection}
|
||||||
|
field={field}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</StyledModal>
|
</StyledModal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default translate()(MediaLibraryModal);
|
export default translate()(MediaLibraryModal) as FC<MediaLibraryModalProps>;
|
||||||
|
35
packages/core/src/components/common/image/Image.tsx
Normal file
35
packages/core/src/components/common/image/Image.tsx
Normal file
@ -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<F extends MediaField> {
|
||||||
|
src?: string;
|
||||||
|
alt?: string;
|
||||||
|
collection?: Collection<F>;
|
||||||
|
field?: F;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Image = <F extends MediaField>({ src, alt, collection, field }: ImageProps<F>) => {
|
||||||
|
const entry = useAppSelector(selectEditingDraft);
|
||||||
|
|
||||||
|
const assetSource = useMediaAsset(src, collection, field, entry);
|
||||||
|
|
||||||
|
return <img key="image" role="presentation" src={assetSource} alt={alt} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withMdxImage = <F extends MediaField>({
|
||||||
|
collection,
|
||||||
|
field,
|
||||||
|
}: Omit<ImageProps<F>, 'src' | 'alt'>) => {
|
||||||
|
const MdxImage = ({ src, alt }: Pick<ImageProps<F>, 'src' | 'alt'>) => (
|
||||||
|
<Image src={src} alt={alt} collection={collection} field={field} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return MdxImage;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Image;
|
@ -6,7 +6,7 @@ import { useParams } from 'react-router-dom';
|
|||||||
|
|
||||||
import { getAdditionalLink } from '@staticcms/core/lib/registry';
|
import { getAdditionalLink } from '@staticcms/core/lib/registry';
|
||||||
import MainView from '../App/MainView';
|
import MainView from '../App/MainView';
|
||||||
import Sidebar from '../Collection/Sidebar';
|
import Sidebar from '../collection/Sidebar';
|
||||||
|
|
||||||
import type { ComponentType } from 'react';
|
import type { ComponentType } from 'react';
|
||||||
import type { ConnectedProps } from 'react-redux';
|
import type { ConnectedProps } from 'react-redux';
|
||||||
|
@ -535,6 +535,12 @@ export interface BaseField {
|
|||||||
widget: string;
|
widget: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface MediaField extends BaseField {
|
||||||
|
media_library?: MediaLibrary;
|
||||||
|
media_folder?: string;
|
||||||
|
public_folder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface BooleanField extends BaseField {
|
export interface BooleanField extends BaseField {
|
||||||
widget: 'boolean';
|
widget: 'boolean';
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
@ -572,13 +578,9 @@ export interface DateTimeField extends BaseField {
|
|||||||
picker_utc?: boolean;
|
picker_utc?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FileOrImageField extends BaseField {
|
export interface FileOrImageField extends MediaField {
|
||||||
widget: 'file' | 'image';
|
widget: 'file' | 'image';
|
||||||
default?: string;
|
default?: string;
|
||||||
|
|
||||||
media_library?: MediaLibrary;
|
|
||||||
media_folder?: string;
|
|
||||||
public_folder?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ObjectField<EF extends BaseField = UnknownField> extends BaseField {
|
export interface ObjectField<EF extends BaseField = UnknownField> extends BaseField {
|
||||||
@ -614,13 +616,9 @@ export interface MapField extends BaseField {
|
|||||||
height?: string;
|
height?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownField extends BaseField {
|
export interface MarkdownField extends MediaField {
|
||||||
widget: 'markdown';
|
widget: 'markdown';
|
||||||
default?: string;
|
default?: string;
|
||||||
|
|
||||||
media_library?: MediaLibrary;
|
|
||||||
media_folder?: string;
|
|
||||||
public_folder?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NumberField extends BaseField {
|
export interface NumberField extends BaseField {
|
||||||
|
51
packages/core/src/lib/hooks/useEntries.ts
Normal file
51
packages/core/src/lib/hooks/useEntries.ts
Normal file
@ -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]);
|
||||||
|
}
|
16
packages/core/src/lib/hooks/useFilters.ts
Normal file
16
packages/core/src/lib/hooks/useFilters.ts
Normal file
@ -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]);
|
||||||
|
}
|
41
packages/core/src/lib/hooks/useGroups.ts
Normal file
41
packages/core/src/lib/hooks/useGroups.ts
Normal file
@ -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<string, { id: string; label: string; value: string | boolean | undefined }> =
|
||||||
|
{};
|
||||||
|
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]);
|
||||||
|
}
|
@ -2,11 +2,12 @@ import { useEffect, useState } from 'react';
|
|||||||
|
|
||||||
import { emptyAsset, getAsset } from '@staticcms/core/actions/media';
|
import { emptyAsset, getAsset } from '@staticcms/core/actions/media';
|
||||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
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<T extends FileOrImageField | MarkdownField>(
|
export default function useIsMediaAsset<T extends MediaField>(
|
||||||
url: string,
|
url: string,
|
||||||
collection: Collection<T>,
|
collection: Collection<T>,
|
||||||
field: T,
|
field: T,
|
||||||
@ -14,17 +15,22 @@ export default function useIsMediaAsset<T extends FileOrImageField | MarkdownFie
|
|||||||
): boolean {
|
): boolean {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [exists, setExists] = useState(false);
|
const [exists, setExists] = useState(false);
|
||||||
|
const debouncedUrl = useDebounce(url, 200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (isEmpty(debouncedUrl)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const checkMediaExistence = async () => {
|
const checkMediaExistence = async () => {
|
||||||
const asset = await dispatch(getAsset<T>(collection, entry, url, field));
|
const asset = await dispatch(getAsset<T>(collection, entry, debouncedUrl, field));
|
||||||
setExists(
|
setExists(
|
||||||
Boolean(asset && asset !== emptyAsset && isNotEmpty(asset.toString()) && asset.fileObj),
|
Boolean(asset && asset !== emptyAsset && isNotEmpty(asset.toString()) && asset.fileObj),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
checkMediaExistence();
|
checkMediaExistence();
|
||||||
}, [collection, dispatch, entry, field, url]);
|
}, [collection, dispatch, entry, field, debouncedUrl]);
|
||||||
|
|
||||||
return exists;
|
return exists;
|
||||||
}
|
}
|
||||||
|
@ -1,34 +1,39 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
import { emptyAsset, getAsset } from '@staticcms/core/actions/media';
|
import { emptyAsset, getAsset } from '@staticcms/core/actions/media';
|
||||||
import { useAppDispatch } from '@staticcms/core/store/hooks';
|
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<T extends Field>(
|
export default function useMediaAsset<T extends MediaField>(
|
||||||
url: string | undefined,
|
url: string | undefined,
|
||||||
collection?: Collection<T>,
|
collection?: Collection<T>,
|
||||||
field?: T,
|
field?: T,
|
||||||
entry?: Entry,
|
entry?: Entry,
|
||||||
): string {
|
): string {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const [assetSource, setAssetSource] = useState(url);
|
const [assetSource, setAssetSource] = useState('');
|
||||||
|
const debouncedUrl = useDebounce(url, 200);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!url) {
|
if (!debouncedUrl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchMedia = async () => {
|
const fetchMedia = async () => {
|
||||||
const asset = await dispatch(getAsset<T>(collection, entry, url, field));
|
const asset = await dispatch(getAsset<T>(collection, entry, debouncedUrl, field));
|
||||||
if (asset !== emptyAsset) {
|
if (asset !== emptyAsset) {
|
||||||
setAssetSource(asset?.toString() ?? '');
|
setAssetSource(asset?.toString() ?? '');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fetchMedia();
|
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],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,17 +5,17 @@ import { openMediaLibrary, removeInsertedMedia } from '@staticcms/core/actions/m
|
|||||||
import { selectMediaPath } from '@staticcms/core/reducers/selectors/mediaLibrary';
|
import { selectMediaPath } from '@staticcms/core/reducers/selectors/mediaLibrary';
|
||||||
import { useAppDispatch, useAppSelector } from '@staticcms/core/store/hooks';
|
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';
|
import type { MouseEvent } from 'react';
|
||||||
|
|
||||||
export default function useMediaInsert<T extends string | string[]>(
|
export default function useMediaInsert<T extends string | string[], F extends MediaField>(
|
||||||
value: T,
|
value: T,
|
||||||
options: { field?: FileOrImageField | MarkdownField; controlID?: string; forImage?: boolean },
|
options: { collection: Collection<F>; field: F; controlID?: string; forImage?: boolean },
|
||||||
callback: (newValue: T) => void,
|
callback: (newValue: T) => void,
|
||||||
): (e?: MouseEvent) => void {
|
): (e?: MouseEvent) => void {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
const { controlID, field, forImage = false } = options;
|
const { controlID, collection, field, forImage = false } = options;
|
||||||
|
|
||||||
const finalControlID = useMemo(() => controlID ?? uuid(), [controlID]);
|
const finalControlID = useMemo(() => controlID ?? uuid(), [controlID]);
|
||||||
const mediaPathSelector = useMemo(() => selectMediaPath(finalControlID), [finalControlID]);
|
const mediaPathSelector = useMemo(() => selectMediaPath(finalControlID), [finalControlID]);
|
||||||
@ -50,11 +50,12 @@ export default function useMediaInsert<T extends string | string[]>(
|
|||||||
replaceIndex,
|
replaceIndex,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
config,
|
config,
|
||||||
|
collection,
|
||||||
field,
|
field,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[dispatch, finalControlID, forImage, value, config, field],
|
[dispatch, finalControlID, forImage, value, config, collection, field],
|
||||||
);
|
);
|
||||||
|
|
||||||
return handleOpenMediaLibrary;
|
return handleOpenMediaLibrary;
|
||||||
|
22
packages/core/src/lib/hooks/usePublishedEntries.ts
Normal file
22
packages/core/src/lib/hooks/usePublishedEntries.ts
Normal file
@ -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],
|
||||||
|
);
|
||||||
|
}
|
@ -258,6 +258,7 @@ export function selectMediaFolders(config: Config, collection: Collection, entry
|
|||||||
|
|
||||||
return [...new Set(folders)];
|
return [...new Set(folders)];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getFieldsNames(fields: Field[] | undefined, prefix = '') {
|
export function getFieldsNames(fields: Field[] | undefined, prefix = '') {
|
||||||
let names = fields?.map(f => `${prefix}${f.name}`) ?? [];
|
let names = fields?.map(f => `${prefix}${f.name}`) ?? [];
|
||||||
|
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
export default class MediaLibraryCloseEvent extends CustomEvent<{}> {
|
||||||
|
constructor() {
|
||||||
|
super('mediaLibraryClose', {});
|
||||||
|
}
|
||||||
|
}
|
@ -49,12 +49,12 @@ function hasCustomFolder(
|
|||||||
|
|
||||||
if ('files' in collection) {
|
if ('files' in collection) {
|
||||||
const file = getFileField(collection.files, slug);
|
const file = getFileField(collection.files, slug);
|
||||||
if (file && file[folderKey]) {
|
if (file && folderKey in file) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (collection[folderKey]) {
|
if (folderKey in collection) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,7 +72,7 @@ function evaluateFolder(
|
|||||||
|
|
||||||
const collection = { ...c };
|
const collection = { ...c };
|
||||||
// add identity template if doesn't exist
|
// add identity template if doesn't exist
|
||||||
if (!collection[folderKey]) {
|
if (!(folderKey in collection)) {
|
||||||
collection[folderKey] = `{{${folderKey}}}`;
|
collection[folderKey] = `{{${folderKey}}}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,10 +2,12 @@ import { useEffect } from 'react';
|
|||||||
|
|
||||||
import type AlertEvent from './events/AlertEvent';
|
import type AlertEvent from './events/AlertEvent';
|
||||||
import type ConfirmEvent from './events/ConfirmEvent';
|
import type ConfirmEvent from './events/ConfirmEvent';
|
||||||
|
import type MediaLibraryCloseEvent from './events/MediaLibraryCloseEvent';
|
||||||
|
|
||||||
interface EventMap {
|
interface EventMap {
|
||||||
alert: AlertEvent;
|
alert: AlertEvent;
|
||||||
confirm: ConfirmEvent;
|
confirm: ConfirmEvent;
|
||||||
|
mediaLibraryClose: MediaLibraryCloseEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWindowEvent<K extends keyof WindowEventMap>(
|
export function useWindowEvent<K extends keyof WindowEventMap>(
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from '../constants';
|
} from '../constants';
|
||||||
|
|
||||||
import type { MediaLibraryAction } from '../actions/mediaLibrary';
|
import type { MediaLibraryAction } from '../actions/mediaLibrary';
|
||||||
import type { Field, MediaFile, MediaLibraryInstance } from '../interface';
|
import type { Collection, Field, MediaFile, MediaLibraryInstance } from '../interface';
|
||||||
|
|
||||||
export interface MediaLibraryDisplayURL {
|
export interface MediaLibraryDisplayURL {
|
||||||
url?: string;
|
url?: string;
|
||||||
@ -39,6 +39,7 @@ export type MediaLibraryState = {
|
|||||||
page?: number;
|
page?: number;
|
||||||
files?: MediaFile[];
|
files?: MediaFile[];
|
||||||
config: Record<string, unknown>;
|
config: Record<string, unknown>;
|
||||||
|
collection?: Collection;
|
||||||
field?: Field;
|
field?: Field;
|
||||||
value?: string | string[];
|
value?: string | string[];
|
||||||
replaceIndex?: number;
|
replaceIndex?: number;
|
||||||
@ -75,7 +76,8 @@ function mediaLibrary(
|
|||||||
};
|
};
|
||||||
|
|
||||||
case MEDIA_LIBRARY_OPEN: {
|
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 || {};
|
const libConfig = config || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -85,6 +87,7 @@ function mediaLibrary(
|
|||||||
controlID,
|
controlID,
|
||||||
canInsert: !!controlID,
|
canInsert: !!controlID,
|
||||||
config: libConfig,
|
config: libConfig,
|
||||||
|
collection,
|
||||||
field,
|
field,
|
||||||
value,
|
value,
|
||||||
replaceIndex,
|
replaceIndex,
|
||||||
|
@ -1,20 +1,9 @@
|
|||||||
import get from 'lodash/get';
|
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 { SORT_DIRECTION_NONE } from '@staticcms/core/constants';
|
||||||
import { selectSortDataPath } from '@staticcms/core/lib/util/sort.util';
|
|
||||||
|
|
||||||
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
import type { CollectionViewStyle } from '@staticcms/core/constants/collectionViews';
|
||||||
import type {
|
import type { Entry, Group, GroupMap, Sort } from '@staticcms/core/interface';
|
||||||
Collection,
|
|
||||||
Entry,
|
|
||||||
Filter,
|
|
||||||
Group,
|
|
||||||
GroupMap,
|
|
||||||
GroupOfEntries,
|
|
||||||
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) {
|
||||||
@ -22,84 +11,49 @@ export function selectEntriesSort(entries: RootState, collection: string) {
|
|||||||
return sort?.[collection];
|
return sort?.[collection];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectEntriesFilter(entries: RootState, collection: string) {
|
export const selectEntriesFilter = (collectionName: string) => (entries: RootState) => {
|
||||||
const filter = entries.entries.filter as Filter | undefined;
|
return entries.entries.filter?.[collectionName];
|
||||||
return filter?.[collection] || {};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function selectEntriesGroup(entries: RootState, collection: string) {
|
export function selectEntriesGroup(entries: RootState, collection: string) {
|
||||||
const group = entries.entries.group as Group | undefined;
|
const group = entries.entries.group as Group | undefined;
|
||||||
return group?.[collection] || {};
|
return group?.[collection] || {};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectEntriesGroupField(entries: RootState, collection: string) {
|
export const selectEntriesGroupField = (collection: string) => (entries: RootState) => {
|
||||||
const groups = selectEntriesGroup(entries, collection);
|
const groups = selectEntriesGroup(entries, collection);
|
||||||
const value = Object.values(groups ?? {}).find(v => v?.active === true);
|
return Object.values(groups ?? {}).find(v => v?.active === true);
|
||||||
return value;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export function selectEntriesSortFields(entries: RootState, collection: string) {
|
export const selectEntriesSortField = (collectionName: string) => (entries: RootState) => {
|
||||||
const sort = selectEntriesSort(entries, collection);
|
const sort = selectEntriesSort(entries, collectionName);
|
||||||
const values = Object.values(sort ?? {}).filter(v => v?.direction !== SORT_DIRECTION_NONE) || [];
|
return Object.values(sort ?? {}).find(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 function selectViewStyle(entries: RootState): CollectionViewStyle {
|
export function selectViewStyle(entries: RootState): CollectionViewStyle {
|
||||||
return entries.entries.viewStyle;
|
return entries.entries.viewStyle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectEntriesBySlugs(state: RootState) {
|
||||||
|
return state.entries.entities;
|
||||||
|
}
|
||||||
|
|
||||||
export function selectEntry(state: RootState, collection: string, slug: string) {
|
export function selectEntry(state: RootState, collection: string, slug: string) {
|
||||||
return state.entries.entities[`${collection}.${slug}`];
|
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 ?? [];
|
return state.entries.pages[collection]?.ids ?? [];
|
||||||
}
|
};
|
||||||
|
|
||||||
function getPublishedEntries(state: RootState, collectionName: string) {
|
export const selectPublishedEntries = (collectionName: string) => (state: RootState) => {
|
||||||
const slugs = selectPublishedSlugs(state, collectionName);
|
const slugs = selectPublishedSlugs(collectionName)(state);
|
||||||
const entries =
|
return (
|
||||||
slugs && (slugs.map(slug => selectEntry(state, collectionName, slug as string)) as Entry[]);
|
slugs && (slugs.map(slug => selectEntry(state, collectionName, slug as string)) as Entry[])
|
||||||
return entries;
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export function selectEntries(state: RootState, collection: Collection) {
|
export function getGroup(entry: Entry, selectedGroup: GroupMap) {
|
||||||
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) {
|
|
||||||
const label = selectedGroup.label;
|
const label = selectedGroup.label;
|
||||||
const field = selectedGroup.field;
|
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<string, { id: string; label: string; value: string | boolean | undefined }> =
|
|
||||||
{};
|
|
||||||
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) {
|
export function selectEntryByPath(state: RootState, collection: string, path: string) {
|
||||||
const slugs = selectPublishedSlugs(state, collection);
|
const slugs = selectPublishedSlugs(collection)(state);
|
||||||
const entries =
|
const entries =
|
||||||
slugs && (slugs.map(slug => selectEntry(state, collection, slug as string)) as Entry[]);
|
slugs && (slugs.map(slug => selectEntry(state, collection, slug as string)) as Entry[]);
|
||||||
|
|
||||||
|
@ -238,7 +238,7 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
|
|
||||||
const handleOpenMediaLibrary = useMediaInsert(
|
const handleOpenMediaLibrary = useMediaInsert(
|
||||||
internalValue,
|
internalValue,
|
||||||
{ field, controlID },
|
{ collection, field, controlID, forImage },
|
||||||
handleOnChange,
|
handleOnChange,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -312,10 +312,11 @@ const withFileControl = ({ forImage = false }: WithFileControlProps = {}) => {
|
|||||||
replaceIndex: index,
|
replaceIndex: index,
|
||||||
allowMultiple: false,
|
allowMultiple: false,
|
||||||
config,
|
config,
|
||||||
|
collection,
|
||||||
field,
|
field,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[config, controlID, field, openMediaLibrary, internalValue],
|
[openMediaLibrary, controlID, internalValue, config, collection, field],
|
||||||
);
|
);
|
||||||
|
|
||||||
// TODO Readd when multiple uploads is supported
|
// TODO Readd when multiple uploads is supported
|
||||||
|
@ -2,7 +2,7 @@ import { styled } from '@mui/material/styles';
|
|||||||
import partial from 'lodash/partial';
|
import partial from 'lodash/partial';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
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 ListItemTopBar from '@staticcms/core/components/UI/ListItemTopBar';
|
||||||
import Outline from '@staticcms/core/components/UI/Outline';
|
import Outline from '@staticcms/core/components/UI/Outline';
|
||||||
import { colors } from '@staticcms/core/components/UI/styles';
|
import { colors } from '@staticcms/core/components/UI/styles';
|
||||||
|
@ -52,7 +52,7 @@ const ListControlWrapper = createControlWrapper({
|
|||||||
path: 'list',
|
path: 'list',
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('@staticcms/core/components/Editor/EditorControlPane/EditorControl', () => {
|
jest.mock('@staticcms/core/components/editor/EditorControlPane/EditorControl', () => {
|
||||||
return jest.fn(props => {
|
return jest.fn(props => {
|
||||||
const { parentPath, field, value } = props;
|
const { parentPath, field, value } = props;
|
||||||
return (
|
return (
|
||||||
|
@ -7,6 +7,7 @@ import { getShortcodes } from '../../lib/registry';
|
|||||||
import withShortcodeMdxComponent from './mdx/withShortcodeMdxComponent';
|
import withShortcodeMdxComponent from './mdx/withShortcodeMdxComponent';
|
||||||
import useMdx from './plate/hooks/useMdx';
|
import useMdx from './plate/hooks/useMdx';
|
||||||
import { processShortcodeConfigToMdx } from './plate/serialization/slate/processShortcodeConfig';
|
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 { MarkdownField, WidgetPreviewProps } from '@staticcms/core/interface';
|
||||||
import type { FC } from 'react';
|
import type { FC } from 'react';
|
||||||
@ -26,13 +27,14 @@ function FallbackComponent({ error }: FallbackComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewProps => {
|
const MarkdownPreview: FC<WidgetPreviewProps<string, MarkdownField>> = previewProps => {
|
||||||
const { value } = previewProps;
|
const { value, collection, field } = previewProps;
|
||||||
|
|
||||||
const components = useMemo(
|
const components = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
Shortcode: withShortcodeMdxComponent({ previewProps }),
|
Shortcode: withShortcodeMdxComponent({ previewProps }),
|
||||||
|
img: withMdxImage({ collection, field }),
|
||||||
}),
|
}),
|
||||||
[previewProps],
|
[collection, field, previewProps],
|
||||||
);
|
);
|
||||||
|
|
||||||
const [state, setValue] = useMdx(value ?? '');
|
const [state, setValue] = useMdx(value ?? '');
|
||||||
|
@ -5,11 +5,10 @@ import Popper from '@mui/material/Popper';
|
|||||||
import { styled, useTheme } from '@mui/material/styles';
|
import { styled, useTheme } from '@mui/material/styles';
|
||||||
import TextField from '@mui/material/TextField';
|
import TextField from '@mui/material/TextField';
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
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 useIsMediaAsset from '@staticcms/core/lib/hooks/useIsMediaAsset';
|
||||||
import useMediaInsert from '@staticcms/core/lib/hooks/useMediaInsert';
|
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 { Collection, Entry, FileOrImageField, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||||
@ -65,7 +64,6 @@ export interface MediaPopoverProps<T extends FileOrImageField | MarkdownField> {
|
|||||||
onUrlChange: (newValue: string) => void;
|
onUrlChange: (newValue: string) => void;
|
||||||
onTextChange?: (newValue: string) => void;
|
onTextChange?: (newValue: string) => void;
|
||||||
onClose: (shouldFocus: boolean) => void;
|
onClose: (shouldFocus: boolean) => void;
|
||||||
mediaOpen?: boolean;
|
|
||||||
onMediaToggle?: (open: boolean) => void;
|
onMediaToggle?: (open: boolean) => void;
|
||||||
onMediaChange: (newValue: string) => void;
|
onMediaChange: (newValue: string) => void;
|
||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
@ -87,7 +85,6 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
onUrlChange,
|
onUrlChange,
|
||||||
onTextChange,
|
onTextChange,
|
||||||
onClose,
|
onClose,
|
||||||
mediaOpen,
|
|
||||||
onMediaToggle,
|
onMediaToggle,
|
||||||
onMediaChange,
|
onMediaChange,
|
||||||
onRemove,
|
onRemove,
|
||||||
@ -101,9 +98,9 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
|
|
||||||
const [editing, setEditing] = useState(inserting);
|
const [editing, setEditing] = useState(inserting);
|
||||||
|
|
||||||
const hasEditorFocus = useFocused();
|
useWindowEvent('mediaLibraryClose', () => {
|
||||||
const [hasFocus, setHasFocus] = useState(false);
|
onMediaToggle?.(false);
|
||||||
const debouncedHasFocus = useDebounce(hasFocus, 150);
|
});
|
||||||
|
|
||||||
const handleClose = useCallback(
|
const handleClose = useCallback(
|
||||||
(shouldFocus: boolean) => {
|
(shouldFocus: boolean) => {
|
||||||
@ -155,60 +152,11 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
}
|
}
|
||||||
}, [anchorEl, editing, inserting, urlDisabled]);
|
}, [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(() => {
|
const handleFocus = useCallback(() => {
|
||||||
setHasFocus(true);
|
|
||||||
onFocus?.();
|
onFocus?.();
|
||||||
}, [onFocus]);
|
}, [onFocus]);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setHasFocus(false);
|
|
||||||
onBlur?.();
|
onBlur?.();
|
||||||
}, [onBlur]);
|
}, [onBlur]);
|
||||||
|
|
||||||
@ -220,7 +168,11 @@ const MediaPopover = <T extends FileOrImageField | MarkdownField>({
|
|||||||
[onMediaChange, onMediaToggle],
|
[onMediaChange, onMediaToggle],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleOpenMediaLibrary = useMediaInsert(url, { field, forImage }, handleMediaChange);
|
const handleOpenMediaLibrary = useMediaInsert(
|
||||||
|
url,
|
||||||
|
{ collection, field, forImage },
|
||||||
|
handleMediaChange,
|
||||||
|
);
|
||||||
|
|
||||||
const handleMediaOpen = useCallback(() => {
|
const handleMediaOpen = useCallback(() => {
|
||||||
onMediaToggle?.(true);
|
onMediaToggle?.(true);
|
||||||
|
@ -12,6 +12,7 @@ import { useFocused } from 'slate-react';
|
|||||||
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
import useMediaAsset from '@staticcms/core/lib/hooks/useMediaAsset';
|
||||||
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
import { isEmpty } from '@staticcms/core/lib/util/string.util';
|
||||||
import { MediaPopover } from '@staticcms/markdown';
|
import { MediaPopover } from '@staticcms/markdown';
|
||||||
|
import useDebounce from '@staticcms/core/lib/hooks/useDebounce';
|
||||||
|
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { MdImageElement, MdValue } from '@staticcms/markdown';
|
import type { MdImageElement, MdValue } from '@staticcms/markdown';
|
||||||
@ -35,13 +36,32 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
|
|||||||
const { url, alt } = element;
|
const { url, alt } = element;
|
||||||
const [internalUrl, setInternalUrl] = useState(url);
|
const [internalUrl, setInternalUrl] = useState(url);
|
||||||
const [internalAlt, setInternalAlt] = useState(alt);
|
const [internalAlt, setInternalAlt] = useState(alt);
|
||||||
|
const [popoverHasFocus, setPopoverHasFocus] = useState(false);
|
||||||
|
const debouncedPopoverHasFocus = useDebounce(popoverHasFocus, 100);
|
||||||
|
|
||||||
|
const [mediaOpen, setMediaOpen] = useState(false);
|
||||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
const imageRef = useRef<HTMLImageElement | null>(null);
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLImageElement | null>(null);
|
const [anchorEl, setAnchorEl] = useState<HTMLImageElement | null>(null);
|
||||||
const hasEditorFocus = useFocused();
|
const hasEditorFocus = useFocused();
|
||||||
|
const debouncedHasEditorFocus = useDebounce(hasEditorFocus, 100);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
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(
|
const handleChange = useCallback(
|
||||||
@ -96,7 +116,28 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
|
|||||||
const selection = usePlateSelection();
|
const selection = usePlateSelection();
|
||||||
|
|
||||||
useEffect(() => {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,12 +150,24 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node !== element && node !== firstChild) {
|
if (node !== element && node !== firstChild) {
|
||||||
handleClose();
|
if (anchorEl) {
|
||||||
|
handleClose();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleOpenPopover();
|
handleOpenPopover();
|
||||||
}, [handleClose, hasEditorFocus, element, selection, editor, handleOpenPopover]);
|
}, [
|
||||||
|
handleClose,
|
||||||
|
hasEditorFocus,
|
||||||
|
element,
|
||||||
|
selection,
|
||||||
|
editor,
|
||||||
|
handleOpenPopover,
|
||||||
|
mediaOpen,
|
||||||
|
popoverHasFocus,
|
||||||
|
anchorEl,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<span onBlur={handleBlur}>
|
<span onBlur={handleBlur}>
|
||||||
@ -140,6 +193,9 @@ const withImageElement = ({ containerRef, collection, entry, field }: WithImageE
|
|||||||
onMediaChange={handleMediaChange}
|
onMediaChange={handleMediaChange}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
forImage
|
forImage
|
||||||
|
onFocus={handlePopoverFocus}
|
||||||
|
onBlur={handlePopoverBlur}
|
||||||
|
onMediaToggle={handleMediaToggle}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
@ -2,17 +2,21 @@ import {
|
|||||||
findNodePath,
|
findNodePath,
|
||||||
focusEditor,
|
focusEditor,
|
||||||
getEditorString,
|
getEditorString,
|
||||||
|
getNode,
|
||||||
|
replaceNodeChildren,
|
||||||
setNodes,
|
setNodes,
|
||||||
unwrapLink,
|
unwrapLink,
|
||||||
upsertLink,
|
usePlateSelection,
|
||||||
} from '@udecode/plate';
|
} 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 MediaPopover from '../../common/MediaPopover';
|
||||||
|
|
||||||
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
import type { Collection, Entry, MarkdownField } from '@staticcms/core/interface';
|
||||||
import type { MdLinkElement, MdValue } from '@staticcms/markdown';
|
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';
|
import type { FC, MouseEvent } from 'react';
|
||||||
|
|
||||||
export interface WithLinkElementProps {
|
export interface WithLinkElementProps {
|
||||||
@ -24,19 +28,49 @@ export interface WithLinkElementProps {
|
|||||||
|
|
||||||
const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkElementProps) => {
|
const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkElementProps) => {
|
||||||
const LinkElement: FC<PlateRenderElementProps<MdValue, MdLinkElement>> = ({
|
const LinkElement: FC<PlateRenderElementProps<MdValue, MdLinkElement>> = ({
|
||||||
attributes,
|
attributes: { ref: _ref, ...attributes },
|
||||||
children,
|
children,
|
||||||
nodeProps,
|
nodeProps,
|
||||||
element,
|
element,
|
||||||
editor,
|
editor,
|
||||||
}) => {
|
}) => {
|
||||||
const [anchorEl, setAnchorEl] = useState<HTMLAnchorElement | null>(null);
|
const urlRef = useRef<HTMLAnchorElement | null>(null);
|
||||||
|
|
||||||
const { url } = element;
|
const { url } = element;
|
||||||
const path = findNodePath(editor, element);
|
const path = findNodePath(editor, element);
|
||||||
|
|
||||||
const [internalUrl, setInternalUrl] = useState(url);
|
const [internalUrl, setInternalUrl] = useState(url);
|
||||||
const [internalText, setInternalText] = useState(getEditorString(editor, path));
|
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<HTMLAnchorElement | null>(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<HTMLAnchorElement>) => {
|
const handleClick = useCallback((event: MouseEvent<HTMLAnchorElement>) => {
|
||||||
setAnchorEl(event.currentTarget);
|
setAnchorEl(event.currentTarget);
|
||||||
@ -50,11 +84,29 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
|
|||||||
focusEditor(editor, editor.selection);
|
focusEditor(editor, editor.selection);
|
||||||
}, [editor]);
|
}, [editor]);
|
||||||
|
|
||||||
|
const selection = usePlateSelection();
|
||||||
|
|
||||||
const handleChange = useCallback(
|
const handleChange = useCallback(
|
||||||
(newUrl: string, newText: string) => {
|
(newUrl: string, newText: string) => {
|
||||||
const path = findNodePath(editor, element);
|
const path = findNodePath(editor, element);
|
||||||
path && setNodes<MdLinkElement>(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<TText>(editor, {
|
||||||
|
at: path,
|
||||||
|
nodes: { text: newText },
|
||||||
|
insertOptions: {
|
||||||
|
select: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[editor, element],
|
[editor, element],
|
||||||
);
|
);
|
||||||
@ -72,9 +124,83 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
|
|||||||
handleChange(internalUrl, internalText);
|
handleChange(internalUrl, internalText);
|
||||||
}, [handleChange, internalText, internalUrl]);
|
}, [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 (
|
return (
|
||||||
<>
|
<span onBlur={handleBlur}>
|
||||||
<a {...attributes} href={url} {...nodeProps} onClick={handleClick}>
|
<a ref={urlRef} {...attributes} href={url} {...nodeProps} onClick={handleClick}>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
<MediaPopover
|
<MediaPopover
|
||||||
@ -90,8 +216,11 @@ const withLinkElement = ({ containerRef, collection, field, entry }: WithLinkEle
|
|||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
onMediaChange={handleMediaChange}
|
onMediaChange={handleMediaChange}
|
||||||
onRemove={handleRemove}
|
onRemove={handleRemove}
|
||||||
|
onFocus={handlePopoverFocus}
|
||||||
|
onBlur={handlePopoverBlur}
|
||||||
|
onMediaToggle={handleMediaToggle}
|
||||||
/>
|
/>
|
||||||
</>
|
</span>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { styled } from '@mui/material/styles';
|
import { styled } from '@mui/material/styles';
|
||||||
import React, { useCallback, useMemo, useState } from 'react';
|
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 ObjectWidgetTopBar from '@staticcms/core/components/UI/ObjectWidgetTopBar';
|
||||||
import Outline from '@staticcms/core/components/UI/Outline';
|
import Outline from '@staticcms/core/components/UI/Outline';
|
||||||
import { transientOptions } from '@staticcms/core/lib';
|
import { transientOptions } from '@staticcms/core/lib';
|
||||||
|
@ -35,7 +35,7 @@ const ObjectControlWrapper = createControlWrapper({
|
|||||||
path: 'object',
|
path: 'object',
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock('@staticcms/core/components/Editor/EditorControlPane/EditorControl', () => {
|
jest.mock('@staticcms/core/components/editor/EditorControlPane/EditorControl', () => {
|
||||||
return jest.fn(props => {
|
return jest.fn(props => {
|
||||||
const { parentPath, fieldName, field } = props;
|
const { parentPath, fieldName, field } = props;
|
||||||
return (
|
return (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user