fix: path generation and usages in saving new and existing files for nested collections

This commit is contained in:
Daniel Lautzenheiser 2023-04-17 12:11:06 -04:00
parent 2eaecc948e
commit 117320046f
13 changed files with 109 additions and 38 deletions

View File

@ -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,

View File

@ -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) {

View File

@ -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} />}

View File

@ -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,
})}

View File

@ -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' : ''}`,

View File

@ -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 />

View File

@ -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>

View File

@ -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;
}

View File

@ -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}

View File

@ -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[];

View File

@ -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)}`;
}

View File

@ -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 '';
}

View File

@ -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);