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