From 6bcf451a18b88f89f9d9a592395b44f131bd02f9 Mon Sep 17 00:00:00 2001 From: Daniel Lautzenheiser Date: Wed, 6 Sep 2023 12:52:13 -0400 Subject: [PATCH] fix: reapply defaults on discard (#864) --- packages/core/src/actions/entries.ts | 76 ++----------------- .../src/components/entry-editor/Editor.tsx | 5 ++ .../entry-editor/EditorInterface.tsx | 3 + .../components/entry-editor/EditorToolbar.tsx | 9 ++- packages/core/src/lib/util/entry.util.ts | 75 ++++++++++++++++++ packages/core/src/reducers/entryDraft.ts | 12 ++- 6 files changed, 105 insertions(+), 75 deletions(-) create mode 100644 packages/core/src/lib/util/entry.util.ts diff --git a/packages/core/src/actions/entries.ts b/packages/core/src/actions/entries.ts index 8ee8517f1..0f5c39602 100644 --- a/packages/core/src/actions/entries.ts +++ b/packages/core/src/actions/entries.ts @@ -1,5 +1,3 @@ -import isEqual from 'lodash/isEqual'; - import { currentBackend } from '../backend'; import { ADD_DRAFT_ENTRY_MEDIA_FILE, @@ -40,16 +38,11 @@ import { SORT_ENTRIES_SUCCESS, } from '../constants'; import ValidationErrorTypes from '../constants/validationErrorTypes'; -import { - I18N_FIELD_DUPLICATE, - I18N_FIELD_TRANSLATE, - duplicateDefaultI18nFields, - hasI18n, - serializeI18n, -} from '../lib/i18n'; +import { hasI18n, serializeI18n } from '../lib/i18n'; import { serializeValues } from '../lib/serializeEntryValues'; import { Cursor } from '../lib/util'; import { selectFields, updateFieldByKey } from '../lib/util/collection.util'; +import { createEmptyDraftData, createEmptyDraftI18nData } from '../lib/util/entry.util'; import { selectCollectionEntriesCursor } from '../reducers/selectors/cursors'; import { selectEntriesSortField, @@ -77,7 +70,6 @@ import type { FieldError, I18nSettings, ImplementationMediaFile, - ObjectValue, SortDirection, ValueOrNestedValue, ViewFilter, @@ -439,10 +431,10 @@ export function emptyDraftCreated(entry: Entry) { /* * Exported simple Action Creators */ -export function createDraftFromEntry(entry: Entry) { +export function createDraftFromEntry(collection: Collection, entry: Entry) { return { type: DRAFT_CREATE_FROM_ENTRY, - payload: { entry }, + payload: { collection, entry }, } as const; } @@ -625,7 +617,7 @@ export function loadEntry(collection: Collection, slug: string, silent = false) await dispatch(loadMedia()); const loadedEntry = await tryLoadEntry(getState(), collection, slug); dispatch(entryLoaded(collection, loadedEntry)); - dispatch(createDraftFromEntry(loadedEntry)); + dispatch(createDraftFromEntry(collection, loadedEntry)); } catch (error: unknown) { console.error(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 }) { const filesArray = entry.mediaFiles; const assets = filesArray diff --git a/packages/core/src/components/entry-editor/Editor.tsx b/packages/core/src/components/entry-editor/Editor.tsx index b11db2c15..daf96961f 100644 --- a/packages/core/src/components/entry-editor/Editor.tsx +++ b/packages/core/src/components/entry-editor/Editor.tsx @@ -327,6 +327,10 @@ const Editor: FC> = ({ await dispatch(loadScroll()); }, [dispatch]); + const handleDiscardDraft = useCallback(() => { + setVersion(version => version + 1); + }, []); + if (entry && entry.error) { return (
@@ -356,6 +360,7 @@ const Editor: FC> = ({ toggleScroll={handleToggleScroll} scrollSyncActive={scrollSyncActive} loadScroll={handleLoadScroll} + onDiscardDraft={handleDiscardDraft} submitted={submitted} slug={slug} t={t} diff --git a/packages/core/src/components/entry-editor/EditorInterface.tsx b/packages/core/src/components/entry-editor/EditorInterface.tsx index e2159bb15..a9e3b6bc1 100644 --- a/packages/core/src/components/entry-editor/EditorInterface.tsx +++ b/packages/core/src/components/entry-editor/EditorInterface.tsx @@ -76,6 +76,7 @@ interface EditorInterfaceProps { loadScroll: () => void; submitted: boolean; slug: string | undefined; + onDiscardDraft: () => void; } const EditorInterface = ({ @@ -97,6 +98,7 @@ const EditorInterface = ({ toggleScroll, submitted, slug, + onDiscardDraft, }: TranslatedProps) => { const config = useAppSelector(selectConfig); @@ -413,6 +415,7 @@ const EditorInterface = ({ showMobilePreview={showMobilePreview} onMobilePreviewToggle={toggleMobilePreview} className="flex" + onDiscardDraft={onDiscardDraft} /> } > diff --git a/packages/core/src/components/entry-editor/EditorToolbar.tsx b/packages/core/src/components/entry-editor/EditorToolbar.tsx index 275627464..f02a263ee 100644 --- a/packages/core/src/components/entry-editor/EditorToolbar.tsx +++ b/packages/core/src/components/entry-editor/EditorToolbar.tsx @@ -49,6 +49,7 @@ export interface EditorToolbarProps { className?: string; showMobilePreview: boolean; onMobilePreviewToggle: () => void; + onDiscardDraft: () => void; } const EditorToolbar = ({ @@ -75,6 +76,7 @@ const EditorToolbar = ({ className, showMobilePreview, onMobilePreviewToggle, + onDiscardDraft, }: TranslatedProps) => { const canCreate = useMemo( () => ('folder' in collection && collection.create) ?? false, @@ -100,10 +102,11 @@ const EditorToolbar = ({ color: 'warning', }) ) { - dispatch(deleteLocalBackup(collection, slug)); - dispatch(loadEntry(collection, slug)); + await dispatch(deleteLocalBackup(collection, slug)); + await dispatch(loadEntry(collection, slug)); + onDiscardDraft(); } - }, [collection, dispatch, slug]); + }, [collection, dispatch, onDiscardDraft, slug]); const menuItems: JSX.Element[][] = useMemo(() => { const items: JSX.Element[] = []; diff --git a/packages/core/src/lib/util/entry.util.ts b/packages/core/src/lib/util/entry.util.ts new file mode 100644 index 000000000..7720d5691 --- /dev/null +++ b/packages/core/src/lib/util/entry.util.ts @@ -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); +} diff --git a/packages/core/src/reducers/entryDraft.ts b/packages/core/src/reducers/entryDraft.ts index 776c3f19e..bc6da6427 100644 --- a/packages/core/src/reducers/entryDraft.ts +++ b/packages/core/src/reducers/entryDraft.ts @@ -21,6 +21,8 @@ import { REMOVE_DRAFT_ENTRY_MEDIA_FILE, } from '../constants'; 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 type { EntriesAction } from '../actions/entries'; @@ -56,10 +58,18 @@ function entryDraftReducer( 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 return { ...newState, - entry, + entry: { + ...entry, + data: applyDefaultsToDraftData(fields, undefined, entry.data), + }, original: entry, fieldsErrors: {}, hasChanged: false,