fix: folder collection path (#549)
This commit is contained in:
committed by
GitHub
parent
93915dac35
commit
8f7237ab7c
@ -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';
|
||||
|
Reference in New Issue
Block a user