feat: nested collections (#680)
This commit is contained in:
committed by
GitHub
parent
22a1b8d9c0
commit
d0ecae310c
@ -1,11 +1,17 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||
import useIcon from '@staticcms/core/lib/hooks/useIcon';
|
||||
import {
|
||||
selectEntryCollectionTitle,
|
||||
selectFolderEntryExtension,
|
||||
} from '@staticcms/core/lib/util/collection.util';
|
||||
import { addFileTemplateFields } from '@staticcms/core/lib/widgets/stringTemplate';
|
||||
import Button from '../common/button/Button';
|
||||
|
||||
import type { Collection, TranslatedProps } from '@staticcms/core/interface';
|
||||
import type { Collection, Entry, TranslatedProps } from '@staticcms/core/interface';
|
||||
|
||||
interface CollectionHeaderProps {
|
||||
collection: Collection;
|
||||
@ -30,17 +36,54 @@ const CollectionHeader = ({
|
||||
|
||||
const icon = useIcon(collection.icon);
|
||||
|
||||
const params = useParams();
|
||||
const filterTerm = useMemo(() => params['*'], [params]);
|
||||
const entries = useEntries(collection);
|
||||
|
||||
const pluralLabel = useMemo(() => {
|
||||
if ('nested' in collection && collection.nested?.path && filterTerm) {
|
||||
const entriesByPath = entries.reduce((acc, entry) => {
|
||||
acc[entry.path] = entry;
|
||||
return acc;
|
||||
}, {} as Record<string, Entry>);
|
||||
|
||||
const path = filterTerm.split('/');
|
||||
if (path.length > 0) {
|
||||
const extension = selectFolderEntryExtension(collection);
|
||||
|
||||
const finalPathPart = path[path.length - 1];
|
||||
|
||||
let entry =
|
||||
entriesByPath[
|
||||
`${collection.folder}/${finalPathPart}/${collection.nested.path.index_file}.${extension}`
|
||||
];
|
||||
|
||||
if (entry) {
|
||||
entry = {
|
||||
...entry,
|
||||
data: addFileTemplateFields(entry.path, entry.data as Record<string, string>),
|
||||
};
|
||||
const title = selectEntryCollectionTitle(collection, entry);
|
||||
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return collectionLabel;
|
||||
}, [collection, collectionLabel, entries, filterTerm]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-grow gap-4">
|
||||
<h2 className="text-xl font-semibold flex items-center text-gray-800 dark:text-gray-300">
|
||||
<div className="mr-2 flex">{icon}</div>
|
||||
{collectionLabel}
|
||||
{pluralLabel}
|
||||
</h2>
|
||||
{newEntryUrl ? (
|
||||
<Button onClick={onNewClick}>
|
||||
{t('collection.collectionTop.newButton', {
|
||||
collectionLabel: collectionLabelSingular || collectionLabel,
|
||||
collectionLabel: collectionLabelSingular || pluralLabel,
|
||||
})}
|
||||
</Button>
|
||||
) : null}
|
||||
|
40
packages/core/src/components/collections/CollectionPage.tsx
Normal file
40
packages/core/src/components/collections/CollectionPage.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import useBreadcrumbs from '@staticcms/core/lib/hooks/useBreadcrumbs';
|
||||
import MainView from '../MainView';
|
||||
import CollectionView from './CollectionView';
|
||||
|
||||
import type { Collection } from '@staticcms/core/interface';
|
||||
import type { FC } from 'react';
|
||||
|
||||
interface CollectionViewProps {
|
||||
collection: Collection;
|
||||
isSearchResults?: boolean;
|
||||
isSingleSearchResult?: boolean;
|
||||
}
|
||||
|
||||
const CollectionPage: FC<CollectionViewProps> = ({
|
||||
collection,
|
||||
isSearchResults,
|
||||
isSingleSearchResult,
|
||||
}) => {
|
||||
const { name, searchTerm, ...params } = useParams();
|
||||
const filterTerm = params['*'];
|
||||
|
||||
const breadcrumbs = useBreadcrumbs(collection, filterTerm);
|
||||
|
||||
return (
|
||||
<MainView breadcrumbs={breadcrumbs} showQuickCreate showLeftNav>
|
||||
<CollectionView
|
||||
name={name}
|
||||
searchTerm={searchTerm}
|
||||
filterTerm={filterTerm}
|
||||
isSearchResults={isSearchResults}
|
||||
isSingleSearchResult={isSingleSearchResult}
|
||||
/>
|
||||
</MainView>
|
||||
);
|
||||
};
|
||||
|
||||
export default CollectionPage;
|
@ -7,8 +7,9 @@ import {
|
||||
} from '@staticcms/core/reducers/selectors/collections';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import { getDefaultPath } from '../../lib/util/collection.util';
|
||||
import MainView from '../MainView';
|
||||
import Collection from './Collection';
|
||||
import CollectionPage from './CollectionPage';
|
||||
|
||||
import type { Collection } from '@staticcms/core/interface';
|
||||
|
||||
interface CollectionRouteProps {
|
||||
isSearchResults?: boolean;
|
||||
@ -16,8 +17,7 @@ interface CollectionRouteProps {
|
||||
}
|
||||
|
||||
const CollectionRoute = ({ isSearchResults, isSingleSearchResult }: CollectionRouteProps) => {
|
||||
const { name, searchTerm, ...params } = useParams();
|
||||
const filterTerm = params['*'];
|
||||
const { name, searchTerm } = useParams();
|
||||
|
||||
const collectionSelector = useMemo(() => selectCollection(name), [name]);
|
||||
const collection = useAppSelector(collectionSelector);
|
||||
@ -34,15 +34,11 @@ const CollectionRoute = ({ isSearchResults, isSingleSearchResult }: CollectionRo
|
||||
}
|
||||
|
||||
return (
|
||||
<MainView breadcrumbs={[{ name: collection?.label }]} showQuickCreate showLeftNav>
|
||||
<Collection
|
||||
name={name}
|
||||
searchTerm={searchTerm}
|
||||
filterTerm={filterTerm}
|
||||
isSearchResults={isSearchResults}
|
||||
isSingleSearchResult={isSingleSearchResult}
|
||||
/>
|
||||
</MainView>
|
||||
<CollectionPage
|
||||
collection={collection as unknown as Collection}
|
||||
isSearchResults={isSearchResults}
|
||||
isSingleSearchResult={isSingleSearchResult}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -42,7 +42,6 @@ const CollectionView = ({
|
||||
collection,
|
||||
collections,
|
||||
collectionName,
|
||||
// TODO isSearchEnabled,
|
||||
isSearchResults,
|
||||
isSingleSearchResult,
|
||||
searchTerm,
|
||||
@ -72,13 +71,8 @@ const CollectionView = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let url = 'fields' in collection && collection.create ? getNewEntryUrl(collectionName) : '';
|
||||
if (url && filterTerm) {
|
||||
url = `${url}?path=${filterTerm}`;
|
||||
}
|
||||
|
||||
return url;
|
||||
}, [collection, collectionName, filterTerm]);
|
||||
return 'fields' in collection && collection.create ? getNewEntryUrl(collectionName) : '';
|
||||
}, [collection, collectionName]);
|
||||
|
||||
const searchResultKey = useMemo(
|
||||
() => `collection.collectionTop.searchResults${isSingleSearchResult ? 'InCollection' : ''}`,
|
||||
@ -248,7 +242,6 @@ interface CollectionViewOwnProps {
|
||||
|
||||
function mapStateToProps(state: RootState, ownProps: TranslatedProps<CollectionViewOwnProps>) {
|
||||
const { collections } = state;
|
||||
const isSearchEnabled = state.config.config && state.config.config.search != false;
|
||||
const {
|
||||
isSearchResults,
|
||||
isSingleSearchResult,
|
||||
@ -275,7 +268,6 @@ function mapStateToProps(state: RootState, ownProps: TranslatedProps<CollectionV
|
||||
collection,
|
||||
collections,
|
||||
collectionName: name,
|
||||
isSearchEnabled,
|
||||
sort,
|
||||
sortableFields,
|
||||
viewFilters,
|
@ -1,9 +1,12 @@
|
||||
import { Article as ArticleIcon } from '@styled-icons/material/Article';
|
||||
import { ChevronRight as ChevronRightIcon } from '@styled-icons/material/ChevronRight';
|
||||
import sortBy from 'lodash/sortBy';
|
||||
import { dirname, sep } from 'path';
|
||||
import React, { Fragment, useCallback, useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { selectEntryCollectionTitle } from '@staticcms/core/lib/util/collection.util';
|
||||
import { stringTemplate } from '@staticcms/core/lib/widgets';
|
||||
import NavLink from '../navbar/NavLink';
|
||||
@ -61,27 +64,42 @@ const TreeNode = ({ collection, treeData, depth = 0, onToggle }: TreeNodeProps)
|
||||
|
||||
return (
|
||||
<Fragment key={node.path}>
|
||||
<NavLink
|
||||
to={to}
|
||||
onClick={() => onToggle({ node, expanded: !node.expanded })}
|
||||
data-testid={node.path}
|
||||
>
|
||||
{/* TODO $activeClassName="sidebar-active" */}
|
||||
{/* TODO $depth={depth} */}
|
||||
<ArticleIcon className="h-5 w-5" />
|
||||
<div>
|
||||
<div>{title}</div>
|
||||
{hasChildren && (node.expanded ? <div /> : <div />)}
|
||||
<div className={classNames(depth !== 0 && 'ml-8')}>
|
||||
<NavLink
|
||||
to={to}
|
||||
onClick={() => onToggle({ node, expanded: !node.expanded })}
|
||||
data-testid={node.path}
|
||||
icon={<ArticleIcon className={classNames(depth === 0 ? 'h-6 w-6' : 'h-5 w-5')} />}
|
||||
>
|
||||
<div className="flex w-full gap-2 items-center justify-between">
|
||||
<div>{title}</div>
|
||||
{hasChildren && (
|
||||
<ChevronRightIcon
|
||||
className={classNames(
|
||||
node.expanded && 'rotate-90 transform',
|
||||
`
|
||||
transition-transform
|
||||
h-5
|
||||
w-5
|
||||
group-focus-within/active-list:text-blue-500
|
||||
group-hover/active-list:text-blue-500
|
||||
`,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NavLink>
|
||||
<div className="mt-2 space-y-1.5">
|
||||
{node.expanded && (
|
||||
<TreeNode
|
||||
collection={collection}
|
||||
depth={depth + 1}
|
||||
treeData={node.children}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</NavLink>
|
||||
{node.expanded && (
|
||||
<TreeNode
|
||||
collection={collection}
|
||||
depth={depth + 1}
|
||||
treeData={node.children}
|
||||
onToggle={onToggle}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
@ -141,12 +159,12 @@ export function getTreeData(collection: Collection, entries: Entry[]): TreeNodeD
|
||||
isRoot: false,
|
||||
})),
|
||||
...entriesObj.map((e, index) => {
|
||||
let entryMap = entries[index];
|
||||
entryMap = {
|
||||
...entryMap,
|
||||
data: addFileTemplateFields(entryMap.path, entryMap.data as Record<string, string>),
|
||||
let entry = entries[index];
|
||||
entry = {
|
||||
...entry,
|
||||
data: addFileTemplateFields(entry.path, entry.data as Record<string, string>),
|
||||
};
|
||||
const title = selectEntryCollectionTitle(collection, entryMap);
|
||||
const title = selectEntryCollectionTitle(collection, entry);
|
||||
return {
|
||||
...e,
|
||||
title,
|
||||
@ -219,9 +237,11 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
||||
const [selected, setSelected] = useState<TreeNodeData | null>(null);
|
||||
const [useFilter, setUseFilter] = useState(true);
|
||||
|
||||
const [prevCollection, setPrevCollection] = useState(collection);
|
||||
const [prevEntries, setPrevEntries] = useState(entries);
|
||||
const [prevFilterTerm, setPrevFilterTerm] = useState(filterTerm);
|
||||
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
||||
const [prevEntries, setPrevEntries] = useState<Entry[] | null>(null);
|
||||
const [prevFilterTerm, setPrevFilterTerm] = useState<string | null>(null);
|
||||
|
||||
const { pathname } = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (collection !== prevCollection || entries !== prevEntries || filterTerm !== prevFilterTerm) {
|
||||
@ -235,7 +255,12 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
||||
|
||||
const path = `/${filterTerm}`;
|
||||
walk(newTreeData, node => {
|
||||
if (expanded[node.path] || (useFilter && path.startsWith(node.path))) {
|
||||
if (
|
||||
expanded[node.path] ||
|
||||
(useFilter &&
|
||||
path.startsWith(node.path) &&
|
||||
pathname.startsWith(`/collections/${collection.name}`))
|
||||
) {
|
||||
node.expanded = true;
|
||||
}
|
||||
});
|
||||
@ -250,6 +275,7 @@ const NestedCollection = ({ collection, filterTerm }: NestedCollectionProps) =>
|
||||
collection,
|
||||
entries,
|
||||
filterTerm,
|
||||
pathname,
|
||||
prevCollection,
|
||||
prevEntries,
|
||||
prevFilterTerm,
|
||||
|
@ -5,25 +5,25 @@ import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { selectEditingDraft } from '@staticcms/core/reducers/selectors/entryDraft';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
|
||||
import type { Collection, MediaField } from '@staticcms/core/interface';
|
||||
import type { BaseField, Collection, MediaField, UnknownField } from '@staticcms/core/interface';
|
||||
|
||||
export interface ImageProps<F extends MediaField> {
|
||||
export interface ImageProps<EF extends BaseField> {
|
||||
src?: string;
|
||||
alt?: string;
|
||||
className?: string;
|
||||
collection?: Collection<F>;
|
||||
field?: F;
|
||||
collection?: Collection<EF>;
|
||||
field?: MediaField;
|
||||
'data-testid'?: string;
|
||||
}
|
||||
|
||||
const Image = <F extends MediaField>({
|
||||
const Image = <EF extends BaseField = UnknownField>({
|
||||
src,
|
||||
alt,
|
||||
className,
|
||||
collection,
|
||||
field,
|
||||
'data-testid': dataTestId,
|
||||
}: ImageProps<F>) => {
|
||||
}: ImageProps<EF>) => {
|
||||
const entry = useAppSelector(selectEditingDraft);
|
||||
|
||||
const assetSource = useMediaAsset(src, collection, field, entry);
|
||||
@ -40,11 +40,11 @@ const Image = <F extends MediaField>({
|
||||
);
|
||||
};
|
||||
|
||||
export const withMdxImage = <F extends MediaField>({
|
||||
export const withMdxImage = <EF extends BaseField = UnknownField>({
|
||||
collection,
|
||||
field,
|
||||
}: Pick<ImageProps<F>, 'collection' | 'field'>) => {
|
||||
const MdxImage = (props: Omit<ImageProps<F>, 'collection' | 'field'>) => (
|
||||
}: Pick<ImageProps<EF>, 'collection' | 'field'>) => {
|
||||
const MdxImage = (props: Omit<ImageProps<EF>, 'collection' | 'field'>) => (
|
||||
<Image {...props} collection={collection} field={field} />
|
||||
);
|
||||
|
||||
|
@ -2,6 +2,7 @@ import React, { Fragment, isValidElement } from 'react';
|
||||
|
||||
import { resolveWidget } from '@staticcms/core/lib/registry';
|
||||
import { selectField } from '@staticcms/core/lib/util/field.util';
|
||||
import { isNullish } from '@staticcms/core/lib/util/null.util';
|
||||
import { getTypedFieldForValue } from '@staticcms/list/typedListHelpers';
|
||||
import PreviewHOC from './PreviewHOC';
|
||||
|
||||
@ -86,7 +87,7 @@ export default function getWidgetFor(
|
||||
|
||||
let renderedValue: ValueOrNestedValue | ReactNode = value;
|
||||
if (inferredField) {
|
||||
renderedValue = inferredField.defaultPreview(String(value));
|
||||
renderedValue = inferredField.defaultPreview(isNullish(value) ? '' : String(value));
|
||||
} else if (
|
||||
value &&
|
||||
fieldWithWidgets.widget &&
|
||||
@ -103,7 +104,7 @@ export default function getWidgetFor(
|
||||
"
|
||||
>
|
||||
{field.label ?? field.name}:
|
||||
</strong>{' '}
|
||||
</strong>
|
||||
{value}
|
||||
</>
|
||||
</div>
|
||||
|
@ -1,15 +1,17 @@
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ScrollSyncPane } from 'react-scroll-sync';
|
||||
|
||||
import useBreadcrumbs from '@staticcms/core/lib/hooks/useBreadcrumbs';
|
||||
import { getI18nInfo, getPreviewEntry, hasI18n } from '@staticcms/core/lib/i18n';
|
||||
import {
|
||||
getFileFromSlug,
|
||||
selectEntryCollectionTitle,
|
||||
} from '@staticcms/core/lib/util/collection.util';
|
||||
import { customPathFromSlug } from '@staticcms/core/lib/util/nested.util';
|
||||
import MainView from '../MainView';
|
||||
import EditorToolbar from './EditorToolbar';
|
||||
import EditorControlPane from './editor-control-pane/EditorControlPane';
|
||||
import EditorPreviewPane from './editor-preview-pane/EditorPreviewPane';
|
||||
import EditorToolbar from './EditorToolbar';
|
||||
|
||||
import type {
|
||||
Collection,
|
||||
@ -84,7 +86,7 @@ const EditorInterface = ({
|
||||
displayUrl,
|
||||
isNewEntry,
|
||||
isModification,
|
||||
draftKey, // TODO Review usage
|
||||
draftKey,
|
||||
scrollSyncActive,
|
||||
t,
|
||||
loadScroll,
|
||||
@ -232,22 +234,15 @@ const EditorInterface = ({
|
||||
);
|
||||
|
||||
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
||||
const nestedFieldPath = useMemo(
|
||||
() => customPathFromSlug(collection, entry.slug),
|
||||
[collection, entry.slug],
|
||||
);
|
||||
const breadcrumbs = useBreadcrumbs(collection, nestedFieldPath, { isNewEntry, summary, t });
|
||||
|
||||
return (
|
||||
<MainView
|
||||
breadcrumbs={[
|
||||
{
|
||||
name: collection.label,
|
||||
to: `/collections/${collection.name}`,
|
||||
},
|
||||
{
|
||||
name: isNewEntry
|
||||
? t('collection.collectionTop.newButton', {
|
||||
collectionLabel: collection.label_singular || collection.label,
|
||||
})
|
||||
: summary,
|
||||
},
|
||||
]}
|
||||
breadcrumbs={breadcrumbs}
|
||||
noMargin
|
||||
noScroll
|
||||
navbarActions={
|
||||
|
@ -65,6 +65,7 @@ const EditorControl = ({
|
||||
changeDraftField,
|
||||
i18n,
|
||||
fieldName,
|
||||
isMeta = false,
|
||||
}: TranslatedProps<EditorControlProps>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -103,7 +104,6 @@ const EditorControl = ({
|
||||
}
|
||||
|
||||
const validateValue = async () => {
|
||||
console.log('VALIDATING', field.name);
|
||||
const errors = await validate(field, value, widget, t);
|
||||
dispatch(changeDraftFieldValidation(path, errors, i18n));
|
||||
};
|
||||
@ -114,9 +114,9 @@ const EditorControl = ({
|
||||
const handleChangeDraftField = useCallback(
|
||||
(value: ValueOrNestedValue) => {
|
||||
setDirty(true);
|
||||
changeDraftField({ path, field, value, i18n });
|
||||
changeDraftField({ path, field, value, i18n, isMeta });
|
||||
},
|
||||
[changeDraftField, field, i18n, path],
|
||||
[changeDraftField, field, i18n, isMeta, path],
|
||||
);
|
||||
|
||||
const config = useMemo(() => configState.config, [configState.config]);
|
||||
@ -232,6 +232,7 @@ interface EditorControlOwnProps {
|
||||
forSingleList?: boolean;
|
||||
i18n: I18nSettings | undefined;
|
||||
fieldName?: string;
|
||||
isMeta?: boolean;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: RootState, ownProps: EditorControlOwnProps) {
|
||||
|
@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
|
||||
import { getI18nInfo, hasI18n, isFieldTranslatable } from '@staticcms/core/lib/i18n';
|
||||
import classNames from '@staticcms/core/lib/util/classNames.util';
|
||||
import { getFieldValue } from '@staticcms/core/lib/util/field.util';
|
||||
import { customPathFromSlug } from '@staticcms/core/lib/util/nested.util';
|
||||
import EditorControl from './EditorControl';
|
||||
import LocaleDropdown from './LocaleDropdown';
|
||||
|
||||
@ -12,6 +13,7 @@ import type {
|
||||
Field,
|
||||
FieldsErrors,
|
||||
I18nSettings,
|
||||
StringOrTextField,
|
||||
TranslatedProps,
|
||||
} from '@staticcms/core/interface';
|
||||
|
||||
@ -39,6 +41,25 @@ const EditorControlPane = ({
|
||||
onLocaleChange,
|
||||
t,
|
||||
}: TranslatedProps<EditorControlPaneProps>) => {
|
||||
const nestedFieldPath = useMemo(
|
||||
() => customPathFromSlug(collection, entry.slug),
|
||||
[collection, entry.slug],
|
||||
);
|
||||
|
||||
const pathField = useMemo(
|
||||
() =>
|
||||
({
|
||||
name: 'path',
|
||||
label:
|
||||
'nested' in collection && collection.nested?.path?.label
|
||||
? collection.nested.path.label
|
||||
: 'Path',
|
||||
widget: 'string',
|
||||
i18n: 'none',
|
||||
} as StringOrTextField),
|
||||
[collection],
|
||||
);
|
||||
|
||||
const i18n = useMemo(() => {
|
||||
if (hasI18n(collection)) {
|
||||
const { locales, defaultLocale } = getI18nInfo(collection);
|
||||
@ -66,6 +87,7 @@ const EditorControlPane = ({
|
||||
`
|
||||
flex
|
||||
flex-col
|
||||
min-h-full
|
||||
`,
|
||||
!hideBorder &&
|
||||
`
|
||||
@ -91,6 +113,19 @@ const EditorControlPane = ({
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
{'nested' in collection && collection.nested?.path ? (
|
||||
<EditorControl
|
||||
key="entry-path"
|
||||
field={pathField}
|
||||
value={nestedFieldPath}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
locale={locale}
|
||||
parentPath=""
|
||||
i18n={i18n}
|
||||
isMeta
|
||||
/>
|
||||
) : null}
|
||||
{fields.map(field => {
|
||||
const isTranslatable = isFieldTranslatable(field, locale, i18n?.defaultLocale);
|
||||
const key = i18n ? `field-${locale}_${field.name}` : `field-${field.name}`;
|
||||
|
@ -8,7 +8,7 @@ import type { Collection, MediaField, MediaLibrarInsertOptions } from '@staticcm
|
||||
import type { FC } from 'react';
|
||||
|
||||
interface CurrentMediaDetailsProps {
|
||||
collection?: Collection<MediaField>;
|
||||
collection?: Collection;
|
||||
field?: MediaField;
|
||||
canInsert: boolean;
|
||||
url?: string | string[];
|
||||
|
@ -13,14 +13,16 @@ import Pill from '../../common/pill/Pill';
|
||||
import CopyToClipBoardButton from './CopyToClipBoardButton';
|
||||
|
||||
import type {
|
||||
BaseField,
|
||||
Collection,
|
||||
Field,
|
||||
MediaField,
|
||||
MediaLibraryDisplayURL,
|
||||
TranslatedProps,
|
||||
UnknownField,
|
||||
} from '@staticcms/core/interface';
|
||||
import type { FC, KeyboardEvent } from 'react';
|
||||
|
||||
interface MediaLibraryCardProps {
|
||||
interface MediaLibraryCardProps<T extends MediaField, EF extends BaseField = UnknownField> {
|
||||
isSelected?: boolean;
|
||||
displayURL: MediaLibraryDisplayURL;
|
||||
text: string;
|
||||
@ -28,14 +30,14 @@ interface MediaLibraryCardProps {
|
||||
type?: string;
|
||||
isViewableImage: boolean;
|
||||
isDraft?: boolean;
|
||||
collection?: Collection;
|
||||
field?: Field;
|
||||
collection?: Collection<EF>;
|
||||
field?: T;
|
||||
onSelect: () => void;
|
||||
loadDisplayURL: () => void;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
const MediaLibraryCard: FC<TranslatedProps<MediaLibraryCardProps>> = ({
|
||||
const MediaLibraryCard = <T extends MediaField, EF extends BaseField = UnknownField>({
|
||||
isSelected = false,
|
||||
displayURL,
|
||||
text,
|
||||
@ -49,7 +51,7 @@ const MediaLibraryCard: FC<TranslatedProps<MediaLibraryCardProps>> = ({
|
||||
loadDisplayURL,
|
||||
onDelete,
|
||||
t,
|
||||
}) => {
|
||||
}: TranslatedProps<MediaLibraryCardProps<T, EF>>) => {
|
||||
const entry = useAppSelector(selectEditingDraft);
|
||||
const url = useMediaAsset(displayURL.url, collection, field, entry);
|
||||
|
||||
@ -258,4 +260,4 @@ const MediaLibraryCard: FC<TranslatedProps<MediaLibraryCardProps>> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default translate()(MediaLibraryCard) as FC<MediaLibraryCardProps>;
|
||||
export default translate()(MediaLibraryCard) as FC<MediaLibraryCardProps<MediaField, UnknownField>>;
|
||||
|
@ -27,10 +27,10 @@ const linkClassNames = 'btn btn-text-primary w-full justify-start';
|
||||
const NavLink = ({ icon, children, onClick, ...otherProps }: NavLinkProps) => {
|
||||
const content = useMemo(
|
||||
() => (
|
||||
<>
|
||||
<div className="flex w-full gap-3 items-center">
|
||||
<span className="w-6 h-6">{icon}</span>
|
||||
<span className="ml-3">{children}</span>
|
||||
</>
|
||||
<span className="flex-grow">{children}</span>
|
||||
</div>
|
||||
),
|
||||
[children, icon],
|
||||
);
|
||||
|
@ -10,6 +10,7 @@ import { selectCollections } from '@staticcms/core/reducers/selectors/collection
|
||||
import { selectIsSearchEnabled } from '@staticcms/core/reducers/selectors/config';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import CollectionSearch from '../collections/CollectionSearch';
|
||||
import NestedCollection from '../collections/NestedCollection';
|
||||
import NavLink from './NavLink';
|
||||
|
||||
import type { Collection } from '@staticcms/core/interface';
|
||||
@ -17,7 +18,9 @@ import type { FC } from 'react';
|
||||
import type { TranslateProps } from 'react-polyglot';
|
||||
|
||||
const Sidebar: FC<TranslateProps> = ({ t }) => {
|
||||
const { name, searchTerm } = useParams();
|
||||
const { name, searchTerm, ...params } = useParams();
|
||||
const filterTerm = useMemo(() => params['*'] ?? '', [params]);
|
||||
|
||||
const navigate = useNavigate();
|
||||
const isSearchEnabled = useAppSelector(selectIsSearchEnabled);
|
||||
const collections = useAppSelector(selectCollections);
|
||||
@ -35,18 +38,17 @@ const Sidebar: FC<TranslateProps> = ({ t }) => {
|
||||
const collectionName = collection.name;
|
||||
const icon = getIcon(collection.icon);
|
||||
|
||||
// TODO
|
||||
// if ('nested' in collection) {
|
||||
// return (
|
||||
// <li key={`nested-${collectionName}`}>
|
||||
// <NestedCollection
|
||||
// collection={collection}
|
||||
// filterTerm={filterTerm}
|
||||
// data-testid={collectionName}
|
||||
// />
|
||||
// </li>
|
||||
// );
|
||||
// }
|
||||
if ('nested' in collection) {
|
||||
return (
|
||||
<li key={`nested-${collectionName}`}>
|
||||
<NestedCollection
|
||||
collection={collection}
|
||||
filterTerm={filterTerm}
|
||||
data-testid={collectionName}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<NavLink key={collectionName} to={`/collections/${collectionName}`} icon={icon}>
|
||||
@ -54,7 +56,7 @@ const Sidebar: FC<TranslateProps> = ({ t }) => {
|
||||
</NavLink>
|
||||
);
|
||||
}),
|
||||
[collections],
|
||||
[collections, filterTerm],
|
||||
);
|
||||
|
||||
const additionalLinks = useMemo(() => getAdditionalLinks(), []);
|
||||
@ -126,7 +128,7 @@ const Sidebar: FC<TranslateProps> = ({ t }) => {
|
||||
)}
|
||||
{collectionLinks}
|
||||
{links}
|
||||
<NavLink key="Media" to="/media" icon={<PhotoIcon className="h-5 w-5" />}>
|
||||
<NavLink key="Media" to="/media" icon={<PhotoIcon className="h-6 w-6" />}>
|
||||
{t('app.header.media')}
|
||||
</NavLink>
|
||||
</ul>
|
||||
|
Reference in New Issue
Block a user