fix: path generation and usages in saving new and existing files for nested collections
This commit is contained in:
parent
2eaecc948e
commit
117320046f
@ -955,7 +955,11 @@ export function getSerializedEntry(collection: Collection, entry: Entry): Entry
|
||||
return serializedEntry;
|
||||
}
|
||||
|
||||
export function persistEntry(collection: Collection, navigate: NavigateFunction) {
|
||||
export function persistEntry(
|
||||
collection: Collection,
|
||||
rootSlug: string | undefined,
|
||||
navigate: NavigateFunction,
|
||||
) {
|
||||
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
|
||||
const state = getState();
|
||||
const entryDraft = state.entryDraft;
|
||||
@ -1019,6 +1023,7 @@ export function persistEntry(collection: Collection, navigate: NavigateFunction)
|
||||
return backend
|
||||
.persistEntry({
|
||||
config: configState.config,
|
||||
rootSlug,
|
||||
collection,
|
||||
entryDraft: newEntryDraft,
|
||||
assetProxies,
|
||||
|
@ -822,6 +822,7 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
|
||||
|
||||
async persistEntry({
|
||||
config,
|
||||
rootSlug,
|
||||
collection,
|
||||
entryDraft: draft,
|
||||
assetProxies,
|
||||
@ -841,7 +842,7 @@ export class Backend<EF extends BaseField = UnknownField, BC extends BackendClas
|
||||
|
||||
const newEntry = entryDraft.entry.newRecord ?? false;
|
||||
|
||||
const customPath = selectCustomPath(draft.entry, collection);
|
||||
const customPath = selectCustomPath(draft.entry, collection, rootSlug, config);
|
||||
|
||||
let dataFile: DataFile;
|
||||
if (newEntry) {
|
||||
|
@ -206,6 +206,10 @@ const App = ({
|
||||
path="/collections/:name/new"
|
||||
element={<EditorRoute collections={collections} newRecord />}
|
||||
/>
|
||||
<Route
|
||||
path="/collections/:name/new/*"
|
||||
element={<EditorRoute collections={collections} newRecord />}
|
||||
/>
|
||||
<Route
|
||||
path="/collections/:name/entries/*"
|
||||
element={<EditorRoute collections={collections} />}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import { translate } from 'react-polyglot';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import useEntries from '@staticcms/core/lib/hooks/useEntries';
|
||||
import useIcon from '@staticcms/core/lib/hooks/useIcon';
|
||||
@ -24,17 +24,9 @@ const CollectionHeader = ({
|
||||
newEntryUrl,
|
||||
t,
|
||||
}: TranslatedProps<CollectionHeaderProps>) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const collectionLabel = collection.label;
|
||||
const collectionLabelSingular = collection.label_singular;
|
||||
|
||||
const onNewClick = useCallback(() => {
|
||||
if (newEntryUrl) {
|
||||
navigate(newEntryUrl);
|
||||
}
|
||||
}, [navigate, newEntryUrl]);
|
||||
|
||||
const icon = useIcon(collection.icon);
|
||||
|
||||
const params = useParams();
|
||||
@ -89,7 +81,7 @@ const CollectionHeader = ({
|
||||
{pluralLabel}
|
||||
</h2>
|
||||
{newEntryUrl ? (
|
||||
<Button onClick={onNewClick}>
|
||||
<Button to={newEntryUrl}>
|
||||
{t('collection.collectionTop.newButton', {
|
||||
collectionLabel: collectionLabelSingular || pluralLabel,
|
||||
})}
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
selectViewFilters,
|
||||
selectViewGroups,
|
||||
} from '@staticcms/core/lib/util/collection.util';
|
||||
import { isNotEmpty } from '@staticcms/core/lib/util/string.util';
|
||||
import {
|
||||
selectEntriesFilter,
|
||||
selectEntriesGroup,
|
||||
@ -71,8 +72,10 @@ const CollectionView = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return 'fields' in collection && collection.create ? getNewEntryUrl(collectionName) : '';
|
||||
}, [collection, collectionName]);
|
||||
return 'fields' in collection && collection.create
|
||||
? `${getNewEntryUrl(collectionName)}${isNotEmpty(filterTerm) ? `/${filterTerm}` : ''}`
|
||||
: '';
|
||||
}, [collection, collectionName, filterTerm]);
|
||||
|
||||
const searchResultKey = useMemo(
|
||||
() => `collection.collectionTop.searchResults${isSingleSearchResult ? 'InCollection' : ''}`,
|
||||
|
@ -50,6 +50,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
slug,
|
||||
localBackup,
|
||||
scrollSyncActive,
|
||||
newRecord,
|
||||
t,
|
||||
}) => {
|
||||
const [version, setVersion] = useState(0);
|
||||
@ -90,7 +91,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await dispatch(persistEntry(collection, navigate));
|
||||
await dispatch(persistEntry(collection, slug, navigate));
|
||||
setVersion(version + 1);
|
||||
|
||||
deleteBackup();
|
||||
@ -112,7 +113,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
} catch (e) {}
|
||||
}, 100);
|
||||
},
|
||||
[collection, deleteBackup, dispatch, entryDraft.entry, navigate, version],
|
||||
[collection, deleteBackup, dispatch, entryDraft.entry, navigate, slug, version],
|
||||
);
|
||||
|
||||
const handleDuplicateEntry = useCallback(() => {
|
||||
@ -145,7 +146,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
if (!slug) {
|
||||
if (!slug || newRecord) {
|
||||
return navigate(`/collections/${collection.name}`);
|
||||
}
|
||||
|
||||
@ -154,7 +155,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
deleteBackup();
|
||||
return navigate(`/collections/${collection.name}`);
|
||||
}, 0);
|
||||
}, [collection, deleteBackup, dispatch, entryDraft.hasChanged, navigate, slug]);
|
||||
}, [collection, deleteBackup, dispatch, entryDraft.hasChanged, navigate, newRecord, slug]);
|
||||
|
||||
const [prevLocalBackup, setPrevLocalBackup] = useState<
|
||||
| {
|
||||
@ -198,11 +199,11 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
const [prevCollection, setPrevCollection] = useState<Collection | null>(null);
|
||||
const [prevSlug, setPrevSlug] = useState<string | undefined | null>(null);
|
||||
useEffect(() => {
|
||||
if (!slug && prevSlug !== slug) {
|
||||
if (newRecord && slug !== prevSlug) {
|
||||
setTimeout(() => {
|
||||
dispatch(createEmptyDraft(collection, location.search));
|
||||
});
|
||||
} else if (slug && (prevCollection !== collection || prevSlug !== slug)) {
|
||||
} else if (!newRecord && slug && (prevCollection !== collection || prevSlug !== slug)) {
|
||||
setTimeout(() => {
|
||||
dispatch(retrieveLocalBackup(collection, slug));
|
||||
dispatch(loadEntry(collection, slug));
|
||||
@ -211,7 +212,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
|
||||
setPrevCollection(collection);
|
||||
setPrevSlug(slug);
|
||||
}, [collection, entryDraft.entry, prevSlug, prevCollection, slug, dispatch]);
|
||||
}, [collection, entryDraft.entry, prevSlug, prevCollection, slug, dispatch, newRecord]);
|
||||
|
||||
const leaveMessage = useMemo(() => t('editor.editor.onLeavePage'), [t]);
|
||||
|
||||
@ -236,7 +237,12 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
const isPersisting = entryDraft.entry?.isPersisting;
|
||||
const newRecord = entryDraft.entry?.newRecord;
|
||||
const newEntryPath = `/collections/${collection.name}/new`;
|
||||
if (isPersisting && newRecord && location.pathname === newEntryPath && action === 'PUSH') {
|
||||
if (
|
||||
isPersisting &&
|
||||
newRecord &&
|
||||
location.pathname.startsWith(newEntryPath) &&
|
||||
action === 'PUSH'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -293,12 +299,13 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
|
||||
onDuplicate={handleDuplicateEntry}
|
||||
hasChanged={hasChanged}
|
||||
displayUrl={displayUrl}
|
||||
isNewEntry={!slug}
|
||||
isNewEntry={newRecord}
|
||||
isModification={isModification}
|
||||
toggleScroll={handleToggleScroll}
|
||||
scrollSyncActive={scrollSyncActive}
|
||||
loadScroll={handleLoadScroll}
|
||||
submitted={submitted}
|
||||
slug={slug}
|
||||
t={t}
|
||||
/>
|
||||
<MediaLibraryModal />
|
||||
|
@ -70,6 +70,7 @@ interface EditorInterfaceProps {
|
||||
scrollSyncActive: boolean;
|
||||
loadScroll: () => void;
|
||||
submitted: boolean;
|
||||
slug: string | undefined;
|
||||
}
|
||||
|
||||
const EditorInterface = ({
|
||||
@ -90,6 +91,7 @@ const EditorInterface = ({
|
||||
loadScroll,
|
||||
toggleScroll,
|
||||
submitted,
|
||||
slug,
|
||||
}: TranslatedProps<EditorInterfaceProps>) => {
|
||||
const { locales, defaultLocale } = useMemo(() => getI18nInfo(collection), [collection]) ?? {};
|
||||
const [selectedLocale, setSelectedLocale] = useState<string>(locales?.[1] ?? 'en');
|
||||
@ -179,6 +181,7 @@ const EditorInterface = ({
|
||||
locale={defaultLocale}
|
||||
submitted={submitted}
|
||||
hideBorder={!finalPreviewActive && !i18nActive}
|
||||
slug={slug}
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
|
@ -64,6 +64,7 @@ const EditorControl = ({
|
||||
i18n,
|
||||
fieldName,
|
||||
isMeta = false,
|
||||
controlled = false,
|
||||
}: TranslatedProps<EditorControlProps>) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
@ -185,6 +186,7 @@ const EditorControl = ({
|
||||
hasErrors,
|
||||
errors,
|
||||
theme,
|
||||
controlled,
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
@ -232,6 +234,7 @@ interface EditorControlOwnProps {
|
||||
forSingleList?: boolean;
|
||||
i18n: I18nSettings | undefined;
|
||||
fieldName?: string;
|
||||
controlled?: boolean;
|
||||
isMeta?: boolean;
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,9 @@ 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 { getNestedSlug } from '@staticcms/core/lib/util/nested.util';
|
||||
import { selectConfig } from '@staticcms/core/reducers/selectors/config';
|
||||
import { useAppSelector } from '@staticcms/core/store/hooks';
|
||||
import EditorControl from './EditorControl';
|
||||
import LocaleDropdown from './LocaleDropdown';
|
||||
|
||||
@ -26,6 +28,7 @@ export interface EditorControlPaneProps {
|
||||
locale?: string;
|
||||
canChangeLocale?: boolean;
|
||||
hideBorder: boolean;
|
||||
slug?: string;
|
||||
onLocaleChange?: (locale: string) => void;
|
||||
}
|
||||
|
||||
@ -38,14 +41,10 @@ const EditorControlPane = ({
|
||||
locale,
|
||||
canChangeLocale = false,
|
||||
hideBorder,
|
||||
slug,
|
||||
onLocaleChange,
|
||||
t,
|
||||
}: TranslatedProps<EditorControlPaneProps>) => {
|
||||
const nestedFieldPath = useMemo(
|
||||
() => customPathFromSlug(collection, entry.slug),
|
||||
[collection, entry.slug],
|
||||
);
|
||||
|
||||
const pathField = useMemo(
|
||||
() =>
|
||||
({
|
||||
@ -56,10 +55,18 @@ const EditorControlPane = ({
|
||||
: 'Path',
|
||||
widget: 'string',
|
||||
i18n: 'none',
|
||||
hint: ``,
|
||||
} as StringOrTextField),
|
||||
[collection],
|
||||
);
|
||||
|
||||
const config = useAppSelector(selectConfig);
|
||||
|
||||
const defaultNestedPath = useMemo(
|
||||
() => getNestedSlug(collection, entry, slug, config),
|
||||
[collection, config, entry, slug],
|
||||
);
|
||||
|
||||
const i18n = useMemo(() => {
|
||||
if (hasI18n(collection)) {
|
||||
const { locales, defaultLocale } = getI18nInfo(collection);
|
||||
@ -117,12 +124,13 @@ const EditorControlPane = ({
|
||||
<EditorControl
|
||||
key="entry-path"
|
||||
field={pathField}
|
||||
value={entry.meta?.path ?? nestedFieldPath}
|
||||
value={entry.meta?.path ?? defaultNestedPath}
|
||||
fieldsErrors={fieldsErrors}
|
||||
submitted={submitted}
|
||||
locale={locale}
|
||||
parentPath=""
|
||||
i18n={i18n}
|
||||
controlled
|
||||
isMeta
|
||||
/>
|
||||
) : null}
|
||||
|
@ -282,6 +282,7 @@ export interface WidgetControlProps<T, F extends BaseField = UnknownField, EV =
|
||||
t: t;
|
||||
value: T | undefined | null;
|
||||
theme: 'dark' | 'light';
|
||||
controlled: boolean;
|
||||
}
|
||||
|
||||
export interface WidgetPreviewProps<T = unknown, F extends BaseField = UnknownField> {
|
||||
@ -385,6 +386,7 @@ export interface PersistOptions {
|
||||
|
||||
export interface PersistArgs {
|
||||
config: Config;
|
||||
rootSlug: string | undefined;
|
||||
collection: Collection;
|
||||
entryDraft: EntryDraft;
|
||||
assetProxies: AssetProxy[];
|
||||
|
@ -78,6 +78,13 @@ export function selectFileEntryLabel<EF extends BaseField>(
|
||||
export function selectEntryPath<EF extends BaseField>(collection: Collection<EF>, slug: string) {
|
||||
if ('fields' in collection) {
|
||||
const folder = collection.folder.replace(/\/$/, '');
|
||||
|
||||
if (collection.nested) {
|
||||
if (collection.nested.path?.index_file) {
|
||||
return `${folder}/${slug}.${selectFolderEntryExtension(collection)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `${folder}/${slug}.${selectFolderEntryExtension(collection)}`;
|
||||
}
|
||||
|
||||
|
@ -1,18 +1,28 @@
|
||||
import trim from 'lodash/trim';
|
||||
import { basename, dirname, extname, join } from 'path';
|
||||
|
||||
import { selectFolderEntryExtension } from './collection.util';
|
||||
import { sanitizeSlug } from '../urlHelper';
|
||||
import { selectEntryCollectionTitle, selectFolderEntryExtension } from './collection.util';
|
||||
import { isEmpty, isNotEmpty } from './string.util';
|
||||
|
||||
import type { Collection, Entry } from '@staticcms/core/interface';
|
||||
import type { Collection, Config, Entry } from '@staticcms/core/interface';
|
||||
|
||||
export function selectCustomPath(entry: Entry, collection: Collection): string | undefined {
|
||||
if (!('nested' in collection) || !collection.nested?.path || !entry.meta) {
|
||||
export function selectCustomPath(
|
||||
entry: Entry,
|
||||
collection: Collection,
|
||||
rootSlug: string | undefined,
|
||||
config: Config | undefined,
|
||||
): string | undefined {
|
||||
if (!('nested' in collection) || !collection.nested?.path) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const indexFile = collection.nested.path.index_file;
|
||||
const extension = selectFolderEntryExtension(collection);
|
||||
const customPath = join(collection.folder, entry.meta.path, `${indexFile}.${extension}`);
|
||||
|
||||
const slug = entry.meta?.path ?? getNestedSlug(collection, entry, rootSlug, config);
|
||||
|
||||
const customPath = join(collection.folder, slug, `${indexFile}.${extension}`);
|
||||
return customPath;
|
||||
}
|
||||
|
||||
@ -40,3 +50,28 @@ export function slugFromCustomPath(collection: Collection, customPath: string):
|
||||
const slug = join(dirname(trim(entryPath, '/')), basename(entryPath, extname(customPath)));
|
||||
return slug;
|
||||
}
|
||||
|
||||
export function getNestedSlug(
|
||||
collection: Collection,
|
||||
entry: Entry,
|
||||
slug: string | undefined,
|
||||
config: Config | undefined,
|
||||
) {
|
||||
if ('nested' in collection && collection.nested?.path) {
|
||||
if (isNotEmpty(entry.slug)) {
|
||||
return entry.slug.replace(new RegExp(`/${collection.nested.path.index_file}$`, 'g'), '');
|
||||
} else if (slug) {
|
||||
let summarySlug = selectEntryCollectionTitle(collection, entry);
|
||||
if (isEmpty(summarySlug)) {
|
||||
summarySlug = `new-${collection.label_singular ?? collection.label}`;
|
||||
}
|
||||
|
||||
return `${customPathFromSlug(collection, slug)}/${sanitizeSlug(
|
||||
summarySlug.toLowerCase(),
|
||||
config?.slug,
|
||||
)}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
@ -14,12 +14,13 @@ const StringControl: FC<WidgetControlProps<string, StringOrTextField>> = ({
|
||||
field,
|
||||
forSingleList,
|
||||
duplicate,
|
||||
controlled,
|
||||
onChange,
|
||||
}) => {
|
||||
const [internalRawValue, setInternalValue] = useState(value ?? '');
|
||||
const internalValue = useMemo(
|
||||
() => (duplicate ? value ?? '' : internalRawValue),
|
||||
[internalRawValue, duplicate, value],
|
||||
() => (controlled || duplicate ? value ?? '' : internalRawValue),
|
||||
[controlled, duplicate, value, internalRawValue],
|
||||
);
|
||||
const ref = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user