fix: reapply defaults on discard (#864)

This commit is contained in:
Daniel Lautzenheiser 2023-09-06 12:52:13 -04:00 committed by GitHub
parent 5602812774
commit 6bcf451a18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 105 additions and 75 deletions

View File

@ -1,5 +1,3 @@
import isEqual from 'lodash/isEqual';
import { currentBackend } from '../backend'; import { currentBackend } from '../backend';
import { import {
ADD_DRAFT_ENTRY_MEDIA_FILE, ADD_DRAFT_ENTRY_MEDIA_FILE,
@ -40,16 +38,11 @@ import {
SORT_ENTRIES_SUCCESS, SORT_ENTRIES_SUCCESS,
} from '../constants'; } from '../constants';
import ValidationErrorTypes from '../constants/validationErrorTypes'; import ValidationErrorTypes from '../constants/validationErrorTypes';
import { import { hasI18n, serializeI18n } from '../lib/i18n';
I18N_FIELD_DUPLICATE,
I18N_FIELD_TRANSLATE,
duplicateDefaultI18nFields,
hasI18n,
serializeI18n,
} from '../lib/i18n';
import { serializeValues } from '../lib/serializeEntryValues'; import { serializeValues } from '../lib/serializeEntryValues';
import { Cursor } from '../lib/util'; import { Cursor } from '../lib/util';
import { selectFields, updateFieldByKey } from '../lib/util/collection.util'; import { selectFields, updateFieldByKey } from '../lib/util/collection.util';
import { createEmptyDraftData, createEmptyDraftI18nData } from '../lib/util/entry.util';
import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors'; import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors';
import { import {
selectEntriesSortField, selectEntriesSortField,
@ -77,7 +70,6 @@ import type {
FieldError, FieldError,
I18nSettings, I18nSettings,
ImplementationMediaFile, ImplementationMediaFile,
ObjectValue,
SortDirection, SortDirection,
ValueOrNestedValue, ValueOrNestedValue,
ViewFilter, ViewFilter,
@ -439,10 +431,10 @@ export function emptyDraftCreated(entry: Entry) {
/* /*
* Exported simple Action Creators * Exported simple Action Creators
*/ */
export function createDraftFromEntry(entry: Entry) { export function createDraftFromEntry(collection: Collection, entry: Entry) {
return { return {
type: DRAFT_CREATE_FROM_ENTRY, type: DRAFT_CREATE_FROM_ENTRY,
payload: { entry }, payload: { collection, entry },
} as const; } as const;
} }
@ -625,7 +617,7 @@ export function loadEntry(collection: Collection, slug: string, silent = false)
await dispatch(loadMedia()); await dispatch(loadMedia());
const loadedEntry = await tryLoadEntry(getState(), collection, slug); const loadedEntry = await tryLoadEntry(getState(), collection, slug);
dispatch(entryLoaded(collection, loadedEntry)); dispatch(entryLoaded(collection, loadedEntry));
dispatch(createDraftFromEntry(loadedEntry)); dispatch(createDraftFromEntry(collection, loadedEntry));
} catch (error: unknown) { } catch (error: unknown) {
console.error(error); console.error(error);
if (error instanceof Error) { if (error instanceof Error) {
@ -878,64 +870,6 @@ export function createEmptyDraft(collection: Collection, search: string) {
}; };
} }
export function createEmptyDraftData(
fields: Field[],
skipField: (field: Field) => boolean = () => false,
) {
const ddd = fields.reduce((acc, item) => {
if (skipField(item)) {
return acc;
}
const subfields = 'fields' in item && item.fields;
const list = item.widget === 'list';
const name = item.name;
const defaultValue = (('default' in item ? item.default : null) ?? null) as EntryData;
function isEmptyDefaultValue(val: EntryData | EntryData[]) {
return [[{}], {}].some(e => isEqual(val, e));
}
if (subfields) {
if (list && Array.isArray(defaultValue)) {
acc[name] = defaultValue;
} else {
const asList = Array.isArray(subfields) ? subfields : [subfields];
const subDefaultValue = list
? [createEmptyDraftData(asList, skipField)]
: createEmptyDraftData(asList, skipField);
if (!isEmptyDefaultValue(subDefaultValue)) {
acc[name] = subDefaultValue;
}
}
return acc;
}
if (defaultValue !== null) {
acc[name] = defaultValue;
}
return acc;
}, {} as ObjectValue);
return ddd;
}
function createEmptyDraftI18nData(collection: Collection, dataFields: Field[]) {
if (!hasI18n(collection)) {
return {};
}
function skipField(field: Field) {
return field.i18n !== I18N_FIELD_DUPLICATE && field.i18n !== I18N_FIELD_TRANSLATE;
}
const i18nData = createEmptyDraftData(dataFields, skipField);
return duplicateDefaultI18nFields(collection, i18nData);
}
export function getMediaAssets({ entry }: { entry: Entry }) { export function getMediaAssets({ entry }: { entry: Entry }) {
const filesArray = entry.mediaFiles; const filesArray = entry.mediaFiles;
const assets = filesArray const assets = filesArray

View File

@ -327,6 +327,10 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
await dispatch(loadScroll()); await dispatch(loadScroll());
}, [dispatch]); }, [dispatch]);
const handleDiscardDraft = useCallback(() => {
setVersion(version => version + 1);
}, []);
if (entry && entry.error) { if (entry && entry.error) {
return ( return (
<div> <div>
@ -356,6 +360,7 @@ const Editor: FC<TranslatedProps<EditorProps>> = ({
toggleScroll={handleToggleScroll} toggleScroll={handleToggleScroll}
scrollSyncActive={scrollSyncActive} scrollSyncActive={scrollSyncActive}
loadScroll={handleLoadScroll} loadScroll={handleLoadScroll}
onDiscardDraft={handleDiscardDraft}
submitted={submitted} submitted={submitted}
slug={slug} slug={slug}
t={t} t={t}

View File

@ -76,6 +76,7 @@ interface EditorInterfaceProps {
loadScroll: () => void; loadScroll: () => void;
submitted: boolean; submitted: boolean;
slug: string | undefined; slug: string | undefined;
onDiscardDraft: () => void;
} }
const EditorInterface = ({ const EditorInterface = ({
@ -97,6 +98,7 @@ const EditorInterface = ({
toggleScroll, toggleScroll,
submitted, submitted,
slug, slug,
onDiscardDraft,
}: TranslatedProps<EditorInterfaceProps>) => { }: TranslatedProps<EditorInterfaceProps>) => {
const config = useAppSelector(selectConfig); const config = useAppSelector(selectConfig);
@ -413,6 +415,7 @@ const EditorInterface = ({
showMobilePreview={showMobilePreview} showMobilePreview={showMobilePreview}
onMobilePreviewToggle={toggleMobilePreview} onMobilePreviewToggle={toggleMobilePreview}
className="flex" className="flex"
onDiscardDraft={onDiscardDraft}
/> />
} }
> >

View File

@ -49,6 +49,7 @@ export interface EditorToolbarProps {
className?: string; className?: string;
showMobilePreview: boolean; showMobilePreview: boolean;
onMobilePreviewToggle: () => void; onMobilePreviewToggle: () => void;
onDiscardDraft: () => void;
} }
const EditorToolbar = ({ const EditorToolbar = ({
@ -75,6 +76,7 @@ const EditorToolbar = ({
className, className,
showMobilePreview, showMobilePreview,
onMobilePreviewToggle, onMobilePreviewToggle,
onDiscardDraft,
}: TranslatedProps<EditorToolbarProps>) => { }: TranslatedProps<EditorToolbarProps>) => {
const canCreate = useMemo( const canCreate = useMemo(
() => ('folder' in collection && collection.create) ?? false, () => ('folder' in collection && collection.create) ?? false,
@ -100,10 +102,11 @@ const EditorToolbar = ({
color: 'warning', color: 'warning',
}) })
) { ) {
dispatch(deleteLocalBackup(collection, slug)); await dispatch(deleteLocalBackup(collection, slug));
dispatch(loadEntry(collection, slug)); await dispatch(loadEntry(collection, slug));
onDiscardDraft();
} }
}, [collection, dispatch, slug]); }, [collection, dispatch, onDiscardDraft, slug]);
const menuItems: JSX.Element[][] = useMemo(() => { const menuItems: JSX.Element[][] = useMemo(() => {
const items: JSX.Element[] = []; const items: JSX.Element[] = [];

View File

@ -0,0 +1,75 @@
import isEqual from 'lodash/isEqual';
import { isNotNullish } from './null.util';
import {
I18N_FIELD_DUPLICATE,
I18N_FIELD_TRANSLATE,
duplicateDefaultI18nFields,
hasI18n,
} from '../i18n';
import type { Collection, EntryData, Field, ObjectValue } from '@staticcms/core/interface';
export function applyDefaultsToDraftData(
fields: Field[],
skipField: (field: Field) => boolean = () => false,
initialValue?: ObjectValue | null,
) {
const emptyDraftData = fields.reduce((acc, item) => {
const name = item.name;
if (skipField(item) || isNotNullish(acc[name])) {
return acc;
}
const subfields = 'fields' in item && item.fields;
const list = item.widget === 'list';
const defaultValue = (('default' in item ? item.default : null) ?? null) as EntryData;
function isEmptyDefaultValue(val: EntryData | EntryData[]) {
return [[{}], {}].some(e => isEqual(val, e));
}
if (subfields) {
if (list && Array.isArray(defaultValue)) {
acc[name] = defaultValue;
} else {
const asList = Array.isArray(subfields) ? subfields : [subfields];
const subDefaultValue = list
? [applyDefaultsToDraftData(asList, skipField)]
: applyDefaultsToDraftData(asList, skipField);
if (!isEmptyDefaultValue(subDefaultValue)) {
acc[name] = subDefaultValue;
}
}
return acc;
}
if (defaultValue !== null) {
acc[name] = defaultValue;
}
return acc;
}, (initialValue ?? {}) as ObjectValue);
return emptyDraftData;
}
export function createEmptyDraftData(fields: Field[], skipField?: (field: Field) => boolean) {
return applyDefaultsToDraftData(fields, skipField);
}
export function createEmptyDraftI18nData(collection: Collection, dataFields: Field[]) {
if (!hasI18n(collection)) {
return {};
}
function skipField(field: Field) {
return field.i18n !== I18N_FIELD_DUPLICATE && field.i18n !== I18N_FIELD_TRANSLATE;
}
const i18nData = createEmptyDraftData(dataFields, skipField);
return duplicateDefaultI18nFields(collection, i18nData);
}

View File

@ -21,6 +21,8 @@ import {
REMOVE_DRAFT_ENTRY_MEDIA_FILE, REMOVE_DRAFT_ENTRY_MEDIA_FILE,
} from '../constants'; } from '../constants';
import { duplicateI18nFields, getDataPath } from '../lib/i18n'; import { duplicateI18nFields, getDataPath } from '../lib/i18n';
import { fileForEntry } from '../lib/util/collection.util';
import { applyDefaultsToDraftData } from '../lib/util/entry.util';
import { set } from '../lib/util/object.util'; import { set } from '../lib/util/object.util';
import type { EntriesAction } from '../actions/entries'; import type { EntriesAction } from '../actions/entries';
@ -56,10 +58,18 @@ function entryDraftReducer(
newRecord: false, newRecord: false,
}; };
const collection = action.payload.collection;
const file = fileForEntry(collection, entry.slug);
const fields = file ? file.fields : 'fields' in collection ? collection.fields : [];
// Existing Entry // Existing Entry
return { return {
...newState, ...newState,
entry, entry: {
...entry,
data: applyDefaultsToDraftData(fields, undefined, entry.data),
},
original: entry, original: entry,
fieldsErrors: {}, fieldsErrors: {},
hasChanged: false, hasChanged: false,