@10x-media/undo-redo 0.1.0-beta.0 → 0.1.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +21 -21
- package/dist/client/useUndoRedo.js +2 -1
- package/dist/client/useUndoRedo.js.map +1 -1
- package/dist/history/historyCore.js +33 -1
- package/dist/history/historyCore.js.map +1 -1
- package/dist/translations/ar.js +18 -0
- package/dist/translations/ar.js.map +1 -0
- package/dist/translations/es.js +18 -0
- package/dist/translations/es.js.map +1 -0
- package/dist/translations/fr.js +18 -0
- package/dist/translations/fr.js.map +1 -0
- package/dist/translations/id.js +18 -0
- package/dist/translations/id.js.map +1 -0
- package/dist/translations/index.d.ts +10 -1
- package/dist/translations/index.js +19 -1
- package/dist/translations/index.js.map +1 -1
- package/dist/translations/keys.d.ts +1 -1
- package/dist/translations/keys.js +1 -1
- package/dist/translations/keys.js.map +1 -1
- package/dist/translations/ko.js +18 -0
- package/dist/translations/ko.js.map +1 -0
- package/dist/translations/pt.js +18 -0
- package/dist/translations/pt.js.map +1 -0
- package/dist/translations/ru.js +18 -0
- package/dist/translations/ru.js.map +1 -0
- package/dist/translations/uk.js +18 -0
- package/dist/translations/uk.js.map +1 -0
- package/dist/translations/zh.js +18 -0
- package/dist/translations/zh.js.map +1 -0
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @10x-media/undo-redo
|
|
2
2
|
|
|
3
|
+
## 0.1.0-beta.2
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- More built-in locales for the `undoRedo:` strings.
|
|
8
|
+
|
|
9
|
+
- Added: `es`, `fr`, `id`, `pt`, `ru`, `zh`, `uk`, `ar`, `ko`. Every key is covered in each.
|
|
10
|
+
|
|
11
|
+
## 0.1.0-beta.1
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Stop a newly added array or blocks row from costing two undos. Payload adds a row as a blank one and leaves the row's own fields to the debounced form-state request that follows, so on a large config the capture can land between the two waves and record the half-built row as an entry of its own. A capture whose only difference from the current entry is paths that did not exist before is now folded into that entry rather than appended, which is the shape of Payload finishing a job rather than of an edit: every user action that adds paths also changes a row id or the value of the field controlling a condition. Folding rather than discarding keeps the entry able to restore the fields Payload filled in.
|
|
16
|
+
|
|
3
17
|
## 0.1.0-beta.0
|
|
4
18
|
|
|
5
19
|
### Minor Changes
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 10x Media GmbH
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 10x Media GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -109,7 +109,8 @@ const useUndoRedo = ({ captureDebounce = 400, debug = false, ignoreFieldTypes, i
|
|
|
109
109
|
const timer = setTimeout(() => {
|
|
110
110
|
const latest = fieldsRef.current;
|
|
111
111
|
if (!latest) return;
|
|
112
|
-
|
|
112
|
+
pushSnapshot(history, latest);
|
|
113
|
+
bumpRevision();
|
|
113
114
|
refreshFlags();
|
|
114
115
|
}, captureDebounce);
|
|
115
116
|
return () => clearTimeout(timer);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useUndoRedo.js","names":[],"sources":["../../src/client/useUndoRedo.ts"],"sourcesContent":["'use client'\nimport {\n\tuseAllFormFields,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseForm,\n\tuseFormInitializing,\n\tuseFormModified,\n} from '@payloadcms/ui'\nimport type { FormState } from 'payload'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport {\n\tbuildRestoreState,\n\tcanRedo,\n\tcanUndo,\n\tcreateHistory,\n\tisAtSavedState,\n\tMAX_HISTORY_ENTRIES,\n\tmarkSaved,\n\tpushSnapshot,\n\ttype UndoHistory,\n} from '../history/historyCore'\nimport { createPathMatcher } from '../history/pathPatterns'\nimport { createVolatileMatcher } from '../history/volatileValues'\nimport {\n\ttype ControlsClientProps,\n\tDEFAULT_CAPTURE_DEBOUNCE_MS,\n\tDEFAULT_SHORTCUTS,\n} from '../plugin/options'\nimport {\n\tbuildFieldSchemaMap,\n\tcollectIgnorePatterns,\n\ttype FieldSchemaMap,\n} from '../schema/fieldSchema'\n\n/** Shared so a document without a resolvable schema keeps a stable identity. */\nconst EMPTY_SCHEMA: FieldSchemaMap = new Map()\n\n/**\n * Every setting is optional so the hook stays usable when a host calls it by\n * hand rather than through the plugin, which always passes a fully resolved set.\n */\nexport type UseUndoRedoOptions = Partial<ControlsClientProps>\n\n/** The chords actually bound, or null when keyboard handling is off. */\nexport interface BoundChords {\n\tundo: string[]\n\tredo: string[]\n}\n\nexport interface UseUndoRedoResult {\n\t/** True when there is an earlier entry to step back to. */\n\tcanUndo: boolean\n\t/** True when there is a later entry to step forward to. */\n\tcanRedo: boolean\n\tundo: () => void\n\tredo: () => void\n\t/** Restore the entry at `index` in `history.stack`. Out-of-range indexes no-op. */\n\tjumpTo: (index: number) => void\n\t/** Live form state, as `useAllFormFields` reports it. */\n\tfields: FormState\n\t/**\n\t * The mutable history object backing this instance.\n\t *\n\t * Exposed so a custom control can mount `<HistoryDebugOverlay />` or inspect\n\t * the stack. It is an internal structure, not a stable contract: treat it as\n\t * read-only and expect its shape to change in a minor release.\n\t */\n\thistory: UndoHistory\n\t/**\n\t * Counter bumped on every capture and restore. Only moves when `debug` is on,\n\t * since it exists to pull the mutable history into React's render cycle for\n\t * the inspector and nothing else needs the re-renders.\n\t */\n\trevision: number\n\t/** False under autosave, where the saved baseline is deliberately not tracked. */\n\ttracksSavedState: boolean\n\t/** Resolved keyboard chords, for labelling. Null when `shortcuts` is false. */\n\tchords: BoundChords | null\n}\n\n/**\n * Undo/redo for the surrounding Payload form: a client-side history of\n * form-state snapshots, independent of Payload's document versions, with\n * nothing read from or written to the server until the user saves.\n *\n * Backs `<UndoRedoControls />` and is exported so a host can build its own\n * controls instead (pair it with `autoMount: false`). Must be called from\n * inside a document edit form; outside one it finds no fields and stays\n * disabled.\n *\n * One history per call. Two instances on the same form keep two independent\n * stacks and each captures the other's restores as fresh edits, so mount either\n * ours or yours, not both.\n */\nexport const useUndoRedo = ({\n\tcaptureDebounce = DEFAULT_CAPTURE_DEBOUNCE_MS,\n\tdebug = false,\n\tignoreFieldTypes,\n\tignorePaths,\n\tmaxHistory = MAX_HISTORY_ENTRIES,\n\tshortcuts,\n}: UseUndoRedoOptions = {}): UseUndoRedoResult => {\n\tconst [fields, dispatchFields] = useAllFormFields()\n\tconst { formRef, setModified } = useForm()\n\tconst initializing = useFormInitializing()\n\tconst modified = useFormModified()\n\tconst { getEntityConfig, config } = useConfig()\n\tconst { collectionSlug, globalSlug } = useDocumentInfo()\n\n\t/**\n\t * Ignore patterns from the config, plus the ones derived from the document's\n\t * own schema: fields the host opted out through `admin.custom`, and every\n\t * field of an excluded type. The schema is the only place a mounted\n\t * component can learn field types, since form state omits them unless\n\t * `buildFormState` was called with `includeSchema`, which the edit view never\n\t * does.\n\t */\n\tconst entity = useMemo(\n\t\t() =>\n\t\t\tcollectionSlug\n\t\t\t\t? getEntityConfig({ collectionSlug })\n\t\t\t\t: globalSlug\n\t\t\t\t\t? getEntityConfig({ globalSlug })\n\t\t\t\t\t: null,\n\t\t[collectionSlug, getEntityConfig, globalSlug]\n\t)\n\n\t/**\n\t * Autosave persists every edit continuously, so \"differs from what is\n\t * persisted\" is not a state the editor can meaningfully be in: the baseline\n\t * would move on every autosave, and an undo would immediately trigger one and\n\t * move it again. Tracking it there produces a saved marker that chases the\n\t * user around instead of telling them anything, so the whole baseline\n\t * mechanism is skipped and restores simply report the form as modified.\n\t */\n\tconst tracksSavedState = useMemo(() => {\n\t\tconst drafts = entity?.versions?.drafts\n\t\tconst autosave = typeof drafts === 'object' && drafts !== null ? drafts.autosave : false\n\t\treturn !autosave\n\t}, [entity])\n\n\tconst schema = useMemo(\n\t\t() =>\n\t\t\tentity ? buildFieldSchemaMap(entity.fields, { blocksMap: config.blocksMap }) : EMPTY_SCHEMA,\n\t\t[config.blocksMap, entity]\n\t)\n\n\tconst isIgnored = useMemo(\n\t\t() =>\n\t\t\tcreatePathMatcher([\n\t\t\t\t...(ignorePaths ?? []),\n\t\t\t\t...collectIgnorePatterns(schema, ignoreFieldTypes),\n\t\t\t]),\n\t\t[ignoreFieldTypes, ignorePaths, schema]\n\t)\n\n\tconst isVolatile = useMemo(() => createVolatileMatcher(schema), [schema])\n\n\tconst historyRef = useRef<UndoHistory | null>(null)\n\tif (historyRef.current === null) {\n\t\thistoryRef.current = createHistory({ isIgnored, isVolatile, maxHistory })\n\t}\n\tconst history = historyRef.current\n\t// Kept in sync rather than rebuilt, so changing a setting cannot silently\n\t// drop the entries the editor has already accumulated. Assigned during render\n\t// on purpose: an effect would leave a restore triggered between render and\n\t// commit reading the settings the user has just replaced.\n\thistory.options = { isIgnored, isVolatile, maxHistory }\n\n\tconst fieldsRef = useRef<FormState | null>(null)\n\tconst [flags, setFlags] = useState({ redo: false, undo: false })\n\t/**\n\t * Forces consumers to re-read the mutable history object, which is a ref and\n\t * therefore invisible to React. Only bumped under `debug`, so the default\n\t * build keeps re-rendering purely on the undo/redo flags.\n\t */\n\tconst [revision, setRevision] = useState(0)\n\n\tfieldsRef.current = fields\n\n\tconst bumpRevision = useCallback(() => {\n\t\tif (debug) setRevision((n) => n + 1)\n\t}, [debug])\n\n\tconst refreshFlags = useCallback(() => {\n\t\tconst next = { redo: canRedo(history), undo: canUndo(history) }\n\t\tsetFlags((prev) => (prev.undo === next.undo && prev.redo === next.redo ? prev : next))\n\t}, [history])\n\n\t// Debug/e2e handle: lets tests inspect the history without reaching into React.\n\tuseEffect(() => {\n\t\tconst win = window as Window & { __payloadUndoHistory?: UndoHistory }\n\t\twin.__payloadUndoHistory = history\n\t\treturn () => {\n\t\t\tdelete win.__payloadUndoHistory\n\t\t}\n\t}, [history])\n\n\tuseEffect(() => {\n\t\tif (initializing || !fields || Object.keys(fields).length === 0) return\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst latest = fieldsRef.current\n\t\t\tif (!latest) return\n\t\t\tif (pushSnapshot(history, latest)) bumpRevision()\n\t\t\trefreshFlags()\n\t\t}, captureDebounce)\n\t\treturn () => clearTimeout(timer)\n\t}, [captureDebounce, fields, history, initializing, refreshFlags, bumpRevision])\n\n\t/**\n\t * Set when this instance is the one clearing `modified`, which happens when a\n\t * restore lands back on the saved state.\n\t *\n\t * The baseline is already correct in that case, by definition: the restore\n\t * only reports clean because the entry it landed on matches the baseline.\n\t * Re-recording it there reads live form state while the server merge that\n\t * follows REPLACE_STATE is still in flight, so the baseline lands on a state\n\t * that matches no entry, and moves again once the merge settles.\n\t */\n\tconst clearedByRestoreRef = useRef(false)\n\n\t/**\n\t * Re-baseline on every point Payload itself considers the form clean: a\n\t * successful save or autosave, a reset, and the initial load. Reading\n\t * Payload's own flag rather than watching for a save keeps the baseline\n\t * correct for all of them without re-deriving what \"saved\" means.\n\t *\n\t * This records a baseline and deliberately does not touch the stack. Pushing\n\t * here would run the redo-tail truncation in pushSnapshot, and since redoing\n\t * onto the saved state makes the form clean, it would destroy the very branch\n\t * redo had just stepped into.\n\t */\n\tuseEffect(() => {\n\t\tif (!tracksSavedState) return\n\t\tif (modified) {\n\t\t\tclearedByRestoreRef.current = false\n\t\t\treturn\n\t\t}\n\t\tif (initializing || !fieldsRef.current) return\n\t\tif (clearedByRestoreRef.current) {\n\t\t\tclearedByRestoreRef.current = false\n\t\t\treturn\n\t\t}\n\t\tmarkSaved(history, fieldsRef.current)\n\t\tbumpRevision()\n\t}, [bumpRevision, history, initializing, modified, tracksSavedState])\n\n\t/**\n\t * Restore the entry that `resolveTarget` picks. The target is resolved from\n\t * the index *after* capturing pending edits, not before: capturing can move\n\t * the index, and a relative step must be relative to where the user is.\n\t */\n\tconst applyRestore = useCallback(\n\t\t(resolveTarget: (indexAfterCapture: number) => number) => {\n\t\t\tconst current = fieldsRef.current\n\t\t\tif (!current) return\n\t\t\t// Capture pending (not yet debounced) edits first so undo steps back\n\t\t\t// from what the user actually sees, not from the last capture. Echoes\n\t\t\t// of a previous restore dedupe against the current entry and no-op.\n\t\t\tpushSnapshot(history, current)\n\t\t\tconst target = resolveTarget(history.index)\n\t\t\tconst entry = history.stack[target]\n\t\t\tif (!entry) {\n\t\t\t\trefreshFlags()\n\t\t\t\tbumpRevision()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdispatchFields({\n\t\t\t\ttype: 'REPLACE_STATE',\n\t\t\t\tstate: buildRestoreState(entry, current, isIgnored),\n\t\t\t\toptimize: false,\n\t\t\t})\n\t\t\thistory.index = target\n\t\t\t// Modified drives the \"leave without saving\" prompt and the save\n\t\t\t// button, so it has to answer \"does the form differ from what is\n\t\t\t// persisted\", not \"did something just happen\". Landing back on the\n\t\t\t// saved entry is a return to clean; anywhere else is a real change,\n\t\t\t// and marking it also makes the debounced onChange revalidate the\n\t\t\t// restored state on the server so save and autosave pick it up.\n\t\t\tconst clean = tracksSavedState && isAtSavedState(history)\n\t\t\tclearedByRestoreRef.current = clean\n\t\t\tsetModified(!clean)\n\t\t\trefreshFlags()\n\t\t\tbumpRevision()\n\t\t},\n\t\t[bumpRevision, dispatchFields, history, isIgnored, refreshFlags, setModified, tracksSavedState]\n\t)\n\n\tconst restore = useCallback(\n\t\t(direction: -1 | 1) => applyRestore((index) => index + direction),\n\t\t[applyRestore]\n\t)\n\n\tconst undo = useCallback(() => restore(-1), [restore])\n\tconst redo = useCallback(() => restore(1), [restore])\n\tconst jumpTo = useCallback((index: number) => applyRestore(() => index), [applyRestore])\n\n\t/**\n\t * Several edit forms can be mounted at once (document drawers). Only the\n\t * instance belonging to the form the event happened in reacts; for events\n\t * outside any form, the topmost open drawer wins.\n\t *\n\t * The form element comes from Payload's own form context rather than from a\n\t * DOM ref of our own, so the rule holds wherever a host renders its controls,\n\t * including outside the element it is scoping.\n\t *\n\t * Text-editing surfaces need no check here: react-hotkeys-hook skips form\n\t * tags and contenteditable by default, which is exactly the rule we want,\n\t * since native inputs own the browser's text undo and Lexical owns its own.\n\t */\n\tconst isForAnotherForm = useCallback(\n\t\t(e: KeyboardEvent): boolean => {\n\t\t\tconst ourForm = formRef?.current\n\t\t\tconst target = e.target instanceof HTMLElement ? e.target : null\n\t\t\tif (target && ourForm?.contains(target)) return false\n\t\t\tconst targetForm = target?.closest('form')\n\t\t\tif (targetForm) return targetForm !== ourForm\n\t\t\tconst openDrawers = document.querySelectorAll('.drawer--is-open')\n\t\t\tconst topDrawer = openDrawers[openDrawers.length - 1]\n\t\t\treturn Boolean(topDrawer && ourForm && !topDrawer.contains(ourForm))\n\t\t},\n\t\t[formRef]\n\t)\n\n\tconst chords = useMemo(() => {\n\t\tif (shortcuts === false) return null\n\t\treturn {\n\t\t\tredo: shortcuts?.redo ?? [...DEFAULT_SHORTCUTS.redo],\n\t\t\tundo: shortcuts?.undo ?? [...DEFAULT_SHORTCUTS.undo],\n\t\t}\n\t}, [shortcuts])\n\n\tconst hotkeyOptions = useMemo(\n\t\t() => ({\n\t\t\tenabled: chords !== null,\n\t\t\tignoreEventWhen: isForAnotherForm,\n\t\t\tpreventDefault: true,\n\t\t}),\n\t\t[chords, isForAnotherForm]\n\t)\n\n\tuseHotkeys(chords?.undo ?? [], undo, hotkeyOptions, [undo])\n\n\tuseHotkeys(chords?.redo ?? [], redo, hotkeyOptions, [redo])\n\n\treturn {\n\t\tcanRedo: flags.redo,\n\t\tcanUndo: flags.undo,\n\t\tchords,\n\t\tfields,\n\t\thistory,\n\t\tjumpTo,\n\t\tredo,\n\t\trevision,\n\t\ttracksSavedState,\n\t\tundo,\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"useUndoRedo.js","names":[],"sources":["../../src/client/useUndoRedo.ts"],"sourcesContent":["'use client'\nimport {\n\tuseAllFormFields,\n\tuseConfig,\n\tuseDocumentInfo,\n\tuseForm,\n\tuseFormInitializing,\n\tuseFormModified,\n} from '@payloadcms/ui'\nimport type { FormState } from 'payload'\nimport { useCallback, useEffect, useMemo, useRef, useState } from 'react'\nimport { useHotkeys } from 'react-hotkeys-hook'\n\nimport {\n\tbuildRestoreState,\n\tcanRedo,\n\tcanUndo,\n\tcreateHistory,\n\tisAtSavedState,\n\tMAX_HISTORY_ENTRIES,\n\tmarkSaved,\n\tpushSnapshot,\n\ttype UndoHistory,\n} from '../history/historyCore'\nimport { createPathMatcher } from '../history/pathPatterns'\nimport { createVolatileMatcher } from '../history/volatileValues'\nimport {\n\ttype ControlsClientProps,\n\tDEFAULT_CAPTURE_DEBOUNCE_MS,\n\tDEFAULT_SHORTCUTS,\n} from '../plugin/options'\nimport {\n\tbuildFieldSchemaMap,\n\tcollectIgnorePatterns,\n\ttype FieldSchemaMap,\n} from '../schema/fieldSchema'\n\n/** Shared so a document without a resolvable schema keeps a stable identity. */\nconst EMPTY_SCHEMA: FieldSchemaMap = new Map()\n\n/**\n * Every setting is optional so the hook stays usable when a host calls it by\n * hand rather than through the plugin, which always passes a fully resolved set.\n */\nexport type UseUndoRedoOptions = Partial<ControlsClientProps>\n\n/** The chords actually bound, or null when keyboard handling is off. */\nexport interface BoundChords {\n\tundo: string[]\n\tredo: string[]\n}\n\nexport interface UseUndoRedoResult {\n\t/** True when there is an earlier entry to step back to. */\n\tcanUndo: boolean\n\t/** True when there is a later entry to step forward to. */\n\tcanRedo: boolean\n\tundo: () => void\n\tredo: () => void\n\t/** Restore the entry at `index` in `history.stack`. Out-of-range indexes no-op. */\n\tjumpTo: (index: number) => void\n\t/** Live form state, as `useAllFormFields` reports it. */\n\tfields: FormState\n\t/**\n\t * The mutable history object backing this instance.\n\t *\n\t * Exposed so a custom control can mount `<HistoryDebugOverlay />` or inspect\n\t * the stack. It is an internal structure, not a stable contract: treat it as\n\t * read-only and expect its shape to change in a minor release.\n\t */\n\thistory: UndoHistory\n\t/**\n\t * Counter bumped on every capture and restore. Only moves when `debug` is on,\n\t * since it exists to pull the mutable history into React's render cycle for\n\t * the inspector and nothing else needs the re-renders.\n\t */\n\trevision: number\n\t/** False under autosave, where the saved baseline is deliberately not tracked. */\n\ttracksSavedState: boolean\n\t/** Resolved keyboard chords, for labelling. Null when `shortcuts` is false. */\n\tchords: BoundChords | null\n}\n\n/**\n * Undo/redo for the surrounding Payload form: a client-side history of\n * form-state snapshots, independent of Payload's document versions, with\n * nothing read from or written to the server until the user saves.\n *\n * Backs `<UndoRedoControls />` and is exported so a host can build its own\n * controls instead (pair it with `autoMount: false`). Must be called from\n * inside a document edit form; outside one it finds no fields and stays\n * disabled.\n *\n * One history per call. Two instances on the same form keep two independent\n * stacks and each captures the other's restores as fresh edits, so mount either\n * ours or yours, not both.\n */\nexport const useUndoRedo = ({\n\tcaptureDebounce = DEFAULT_CAPTURE_DEBOUNCE_MS,\n\tdebug = false,\n\tignoreFieldTypes,\n\tignorePaths,\n\tmaxHistory = MAX_HISTORY_ENTRIES,\n\tshortcuts,\n}: UseUndoRedoOptions = {}): UseUndoRedoResult => {\n\tconst [fields, dispatchFields] = useAllFormFields()\n\tconst { formRef, setModified } = useForm()\n\tconst initializing = useFormInitializing()\n\tconst modified = useFormModified()\n\tconst { getEntityConfig, config } = useConfig()\n\tconst { collectionSlug, globalSlug } = useDocumentInfo()\n\n\t/**\n\t * Ignore patterns from the config, plus the ones derived from the document's\n\t * own schema: fields the host opted out through `admin.custom`, and every\n\t * field of an excluded type. The schema is the only place a mounted\n\t * component can learn field types, since form state omits them unless\n\t * `buildFormState` was called with `includeSchema`, which the edit view never\n\t * does.\n\t */\n\tconst entity = useMemo(\n\t\t() =>\n\t\t\tcollectionSlug\n\t\t\t\t? getEntityConfig({ collectionSlug })\n\t\t\t\t: globalSlug\n\t\t\t\t\t? getEntityConfig({ globalSlug })\n\t\t\t\t\t: null,\n\t\t[collectionSlug, getEntityConfig, globalSlug]\n\t)\n\n\t/**\n\t * Autosave persists every edit continuously, so \"differs from what is\n\t * persisted\" is not a state the editor can meaningfully be in: the baseline\n\t * would move on every autosave, and an undo would immediately trigger one and\n\t * move it again. Tracking it there produces a saved marker that chases the\n\t * user around instead of telling them anything, so the whole baseline\n\t * mechanism is skipped and restores simply report the form as modified.\n\t */\n\tconst tracksSavedState = useMemo(() => {\n\t\tconst drafts = entity?.versions?.drafts\n\t\tconst autosave = typeof drafts === 'object' && drafts !== null ? drafts.autosave : false\n\t\treturn !autosave\n\t}, [entity])\n\n\tconst schema = useMemo(\n\t\t() =>\n\t\t\tentity ? buildFieldSchemaMap(entity.fields, { blocksMap: config.blocksMap }) : EMPTY_SCHEMA,\n\t\t[config.blocksMap, entity]\n\t)\n\n\tconst isIgnored = useMemo(\n\t\t() =>\n\t\t\tcreatePathMatcher([\n\t\t\t\t...(ignorePaths ?? []),\n\t\t\t\t...collectIgnorePatterns(schema, ignoreFieldTypes),\n\t\t\t]),\n\t\t[ignoreFieldTypes, ignorePaths, schema]\n\t)\n\n\tconst isVolatile = useMemo(() => createVolatileMatcher(schema), [schema])\n\n\tconst historyRef = useRef<UndoHistory | null>(null)\n\tif (historyRef.current === null) {\n\t\thistoryRef.current = createHistory({ isIgnored, isVolatile, maxHistory })\n\t}\n\tconst history = historyRef.current\n\t// Kept in sync rather than rebuilt, so changing a setting cannot silently\n\t// drop the entries the editor has already accumulated. Assigned during render\n\t// on purpose: an effect would leave a restore triggered between render and\n\t// commit reading the settings the user has just replaced.\n\thistory.options = { isIgnored, isVolatile, maxHistory }\n\n\tconst fieldsRef = useRef<FormState | null>(null)\n\tconst [flags, setFlags] = useState({ redo: false, undo: false })\n\t/**\n\t * Forces consumers to re-read the mutable history object, which is a ref and\n\t * therefore invisible to React. Only bumped under `debug`, so the default\n\t * build keeps re-rendering purely on the undo/redo flags.\n\t */\n\tconst [revision, setRevision] = useState(0)\n\n\tfieldsRef.current = fields\n\n\tconst bumpRevision = useCallback(() => {\n\t\tif (debug) setRevision((n) => n + 1)\n\t}, [debug])\n\n\tconst refreshFlags = useCallback(() => {\n\t\tconst next = { redo: canRedo(history), undo: canUndo(history) }\n\t\tsetFlags((prev) => (prev.undo === next.undo && prev.redo === next.redo ? prev : next))\n\t}, [history])\n\n\t// Debug/e2e handle: lets tests inspect the history without reaching into React.\n\tuseEffect(() => {\n\t\tconst win = window as Window & { __payloadUndoHistory?: UndoHistory }\n\t\twin.__payloadUndoHistory = history\n\t\treturn () => {\n\t\t\tdelete win.__payloadUndoHistory\n\t\t}\n\t}, [history])\n\n\tuseEffect(() => {\n\t\tif (initializing || !fields || Object.keys(fields).length === 0) return\n\t\tconst timer = setTimeout(() => {\n\t\t\tconst latest = fieldsRef.current\n\t\t\tif (!latest) return\n\t\t\t// Bumped whether or not an entry was appended: a capture that folds\n\t\t\t// into the current entry changes what the inspector is showing just as\n\t\t\t// much as one that adds a row to it.\n\t\t\tpushSnapshot(history, latest)\n\t\t\tbumpRevision()\n\t\t\trefreshFlags()\n\t\t}, captureDebounce)\n\t\treturn () => clearTimeout(timer)\n\t}, [captureDebounce, fields, history, initializing, refreshFlags, bumpRevision])\n\n\t/**\n\t * Set when this instance is the one clearing `modified`, which happens when a\n\t * restore lands back on the saved state.\n\t *\n\t * The baseline is already correct in that case, by definition: the restore\n\t * only reports clean because the entry it landed on matches the baseline.\n\t * Re-recording it there reads live form state while the server merge that\n\t * follows REPLACE_STATE is still in flight, so the baseline lands on a state\n\t * that matches no entry, and moves again once the merge settles.\n\t */\n\tconst clearedByRestoreRef = useRef(false)\n\n\t/**\n\t * Re-baseline on every point Payload itself considers the form clean: a\n\t * successful save or autosave, a reset, and the initial load. Reading\n\t * Payload's own flag rather than watching for a save keeps the baseline\n\t * correct for all of them without re-deriving what \"saved\" means.\n\t *\n\t * This records a baseline and deliberately does not touch the stack. Pushing\n\t * here would run the redo-tail truncation in pushSnapshot, and since redoing\n\t * onto the saved state makes the form clean, it would destroy the very branch\n\t * redo had just stepped into.\n\t */\n\tuseEffect(() => {\n\t\tif (!tracksSavedState) return\n\t\tif (modified) {\n\t\t\tclearedByRestoreRef.current = false\n\t\t\treturn\n\t\t}\n\t\tif (initializing || !fieldsRef.current) return\n\t\tif (clearedByRestoreRef.current) {\n\t\t\tclearedByRestoreRef.current = false\n\t\t\treturn\n\t\t}\n\t\tmarkSaved(history, fieldsRef.current)\n\t\tbumpRevision()\n\t}, [bumpRevision, history, initializing, modified, tracksSavedState])\n\n\t/**\n\t * Restore the entry that `resolveTarget` picks. The target is resolved from\n\t * the index *after* capturing pending edits, not before: capturing can move\n\t * the index, and a relative step must be relative to where the user is.\n\t */\n\tconst applyRestore = useCallback(\n\t\t(resolveTarget: (indexAfterCapture: number) => number) => {\n\t\t\tconst current = fieldsRef.current\n\t\t\tif (!current) return\n\t\t\t// Capture pending (not yet debounced) edits first so undo steps back\n\t\t\t// from what the user actually sees, not from the last capture. Echoes\n\t\t\t// of a previous restore dedupe against the current entry and no-op.\n\t\t\tpushSnapshot(history, current)\n\t\t\tconst target = resolveTarget(history.index)\n\t\t\tconst entry = history.stack[target]\n\t\t\tif (!entry) {\n\t\t\t\trefreshFlags()\n\t\t\t\tbumpRevision()\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdispatchFields({\n\t\t\t\ttype: 'REPLACE_STATE',\n\t\t\t\tstate: buildRestoreState(entry, current, isIgnored),\n\t\t\t\toptimize: false,\n\t\t\t})\n\t\t\thistory.index = target\n\t\t\t// Modified drives the \"leave without saving\" prompt and the save\n\t\t\t// button, so it has to answer \"does the form differ from what is\n\t\t\t// persisted\", not \"did something just happen\". Landing back on the\n\t\t\t// saved entry is a return to clean; anywhere else is a real change,\n\t\t\t// and marking it also makes the debounced onChange revalidate the\n\t\t\t// restored state on the server so save and autosave pick it up.\n\t\t\tconst clean = tracksSavedState && isAtSavedState(history)\n\t\t\tclearedByRestoreRef.current = clean\n\t\t\tsetModified(!clean)\n\t\t\trefreshFlags()\n\t\t\tbumpRevision()\n\t\t},\n\t\t[bumpRevision, dispatchFields, history, isIgnored, refreshFlags, setModified, tracksSavedState]\n\t)\n\n\tconst restore = useCallback(\n\t\t(direction: -1 | 1) => applyRestore((index) => index + direction),\n\t\t[applyRestore]\n\t)\n\n\tconst undo = useCallback(() => restore(-1), [restore])\n\tconst redo = useCallback(() => restore(1), [restore])\n\tconst jumpTo = useCallback((index: number) => applyRestore(() => index), [applyRestore])\n\n\t/**\n\t * Several edit forms can be mounted at once (document drawers). Only the\n\t * instance belonging to the form the event happened in reacts; for events\n\t * outside any form, the topmost open drawer wins.\n\t *\n\t * The form element comes from Payload's own form context rather than from a\n\t * DOM ref of our own, so the rule holds wherever a host renders its controls,\n\t * including outside the element it is scoping.\n\t *\n\t * Text-editing surfaces need no check here: react-hotkeys-hook skips form\n\t * tags and contenteditable by default, which is exactly the rule we want,\n\t * since native inputs own the browser's text undo and Lexical owns its own.\n\t */\n\tconst isForAnotherForm = useCallback(\n\t\t(e: KeyboardEvent): boolean => {\n\t\t\tconst ourForm = formRef?.current\n\t\t\tconst target = e.target instanceof HTMLElement ? e.target : null\n\t\t\tif (target && ourForm?.contains(target)) return false\n\t\t\tconst targetForm = target?.closest('form')\n\t\t\tif (targetForm) return targetForm !== ourForm\n\t\t\tconst openDrawers = document.querySelectorAll('.drawer--is-open')\n\t\t\tconst topDrawer = openDrawers[openDrawers.length - 1]\n\t\t\treturn Boolean(topDrawer && ourForm && !topDrawer.contains(ourForm))\n\t\t},\n\t\t[formRef]\n\t)\n\n\tconst chords = useMemo(() => {\n\t\tif (shortcuts === false) return null\n\t\treturn {\n\t\t\tredo: shortcuts?.redo ?? [...DEFAULT_SHORTCUTS.redo],\n\t\t\tundo: shortcuts?.undo ?? [...DEFAULT_SHORTCUTS.undo],\n\t\t}\n\t}, [shortcuts])\n\n\tconst hotkeyOptions = useMemo(\n\t\t() => ({\n\t\t\tenabled: chords !== null,\n\t\t\tignoreEventWhen: isForAnotherForm,\n\t\t\tpreventDefault: true,\n\t\t}),\n\t\t[chords, isForAnotherForm]\n\t)\n\n\tuseHotkeys(chords?.undo ?? [], undo, hotkeyOptions, [undo])\n\n\tuseHotkeys(chords?.redo ?? [], redo, hotkeyOptions, [redo])\n\n\treturn {\n\t\tcanRedo: flags.redo,\n\t\tcanUndo: flags.undo,\n\t\tchords,\n\t\tfields,\n\t\thistory,\n\t\tjumpTo,\n\t\tredo,\n\t\trevision,\n\t\ttracksSavedState,\n\t\tundo,\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -205,10 +205,35 @@ const projectComparable = (history, fields) => {
|
|
|
205
205
|
return out;
|
|
206
206
|
};
|
|
207
207
|
/**
|
|
208
|
+
* True when `to` is `from` plus paths that did not exist before, with nothing
|
|
209
|
+
* that did exist having changed.
|
|
210
|
+
*
|
|
211
|
+
* This is the shape of Payload finishing a job rather than of an edit. Adding a
|
|
212
|
+
* row dispatches `ADD_ROW` with a blank row and leaves the real state to the
|
|
213
|
+
* debounced form-state request that follows (see @payloadcms/ui Form
|
|
214
|
+
* `addFieldRow`), so the row's own fields arrive as a second wave, milliseconds
|
|
215
|
+
* to seconds later depending on how big the config is. Every user action that
|
|
216
|
+
* adds paths brings something else with it: adding a row changes the row ids,
|
|
217
|
+
* revealing a conditional field changes the value that controls it. A wave of
|
|
218
|
+
* pure additions has no such companion, and treating it as an edit costs a
|
|
219
|
+
* second undo to get past a single click.
|
|
220
|
+
*/
|
|
221
|
+
const addsPathsOnly = (from, to) => {
|
|
222
|
+
let added = false;
|
|
223
|
+
for (const path of Object.keys(to)) if (!(path in from)) added = true;
|
|
224
|
+
if (!added) return false;
|
|
225
|
+
return Object.keys(from).every((path) => deepEqual(from[path], to[path]));
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
208
228
|
* Push the current form state onto the history. No-ops (and returns false) when
|
|
209
229
|
* nothing user-visible changed relative to the entry at the current index,
|
|
210
230
|
* which absorbs server-merge echoes after saves/restores. A real change drops
|
|
211
231
|
* the redo tail, appends, and caps the stack at the history's `maxHistory`.
|
|
232
|
+
*
|
|
233
|
+
* A wave of pure path additions is folded into the current entry instead of
|
|
234
|
+
* appended (see addsPathsOnly). Folded rather than dropped, because an entry
|
|
235
|
+
* restores exactly the paths it holds: leaving the new ones out would make
|
|
236
|
+
* stepping back onto that entry delete the fields Payload had just filled in.
|
|
212
237
|
*/
|
|
213
238
|
const pushSnapshot = (history, fields) => {
|
|
214
239
|
const { isIgnored, maxHistory } = history.options;
|
|
@@ -218,7 +243,14 @@ const pushSnapshot = (history, fields) => {
|
|
|
218
243
|
snapshot.fields[path] = carried.field;
|
|
219
244
|
snapshot.comparable[path] = carried.comparable;
|
|
220
245
|
}
|
|
221
|
-
if (current
|
|
246
|
+
if (current) {
|
|
247
|
+
if (deepEqual(current.comparable, snapshot.comparable)) return false;
|
|
248
|
+
if (addsPathsOnly(current.comparable, snapshot.comparable)) {
|
|
249
|
+
current.fields = snapshot.fields;
|
|
250
|
+
current.comparable = snapshot.comparable;
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
222
254
|
history.stack.splice(history.index + 1);
|
|
223
255
|
history.stack.push(snapshot);
|
|
224
256
|
if (history.stack.length > maxHistory) history.stack.splice(0, history.stack.length - maxHistory);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"historyCore.js","names":[],"sources":["../../src/history/historyCore.ts"],"sourcesContent":["import type { FormState } from 'payload'\n\nimport { createPathMatcher, type PathMatcher } from './pathPatterns'\n\n/**\n * Pure, React-free core of the admin undo/redo feature.\n *\n * The admin form keeps its state in a flat `FormState` map (path → FieldState).\n * We snapshot that map on (debounced) user edits and restore old snapshots via\n * the form's `REPLACE_STATE` reducer action. Field states carry non-serializable\n * members (React nodes in `customComponents`, `validate` functions), so snapshots\n * shallow-copy each field state and keep those members by reference: only the\n * top level is protected against later mutation, which matches how the form\n * reducer itself treats field states (immutable-style replacement).\n */\n\n/**\n * Payload's own fields, which no editor edits directly and undo must never\n * touch. They change on save, autosave, publish and token refresh without user\n * interaction, so treating them as edits would create \"phantom\" history entries\n * where undo appears to do nothing, and restoring a stale value could revert a\n * publish (`_status`) or corrupt auth state (`sessions`, `salt`, `hash`).\n *\n * Deliberately limited to fields Payload itself injects. Derived fields that a\n * plugin or project adds are user-facing (a project may well have its own\n * `pathname`), so excluding those is the host's call through `ignorePaths`\n * rather than a default baked in here.\n *\n * Each entry ignores its whole subtree, which is what the pattern matcher does\n * for a pattern that runs out before the path does.\n */\nexport const DEFAULT_IGNORED_PATHS = [\n\t'_status',\n\t'createdAt',\n\t'hash',\n\t'lockUntil',\n\t'loginAttempts',\n\t'resetPasswordExpiration',\n\t'resetPasswordToken',\n\t'salt',\n\t'sessions',\n\t'updatedAt',\n] as const\n\n/** True for paths the undo history ignores entirely (see DEFAULT_IGNORED_PATHS). */\nexport const isIgnoredPath: PathMatcher = createPathMatcher(DEFAULT_IGNORED_PATHS)\n\nexport interface ComparableField {\n\t/** Absent for array and blocks fields, whose `value` carries no information (see extractComparableField). */\n\tvalue?: unknown\n\t/** Row ids in order, which captures array/blocks row additions, deletions and moves. */\n\trowIds?: (string | undefined)[]\n}\n\n/** The slice of form state that counts as a user-visible edit. */\nexport type ComparableState = Record<string, ComparableField>\n\nexport interface HistoryEntry {\n\t/**\n\t * Monotonic id, stable across the stack shifting when the cap evicts the\n\t * oldest entries. Positional indexes are not stable for that reason, so\n\t * anything keyed per entry (React keys, debug UI expansion) uses this.\n\t */\n\tid: number\n\tfields: FormState\n\tcomparable: ComparableState\n}\n\nexport const MAX_HISTORY_ENTRIES = 50\n\n/**\n * True for a field whose current value the history cannot put back.\n *\n * Payload's JSON field is the case this exists for. While its text does not\n * parse, the field writes the raw editor text into form state as a string, and\n * the editor is then rendered from `JSON.stringify(value)`, so dispatching that\n * string back would show it double-encoded rather than as the text the editor\n * had. No value we could dispatch reproduces broken text, so rather than record\n * a state it cannot honour, a capture carries the last value it can (see\n * pushSnapshot).\n */\nexport type VolatileMatcher = (path: string, field: FormState[string]) => boolean\n\n/**\n * Per-history settings. They live on the history object rather than being\n * threaded through every call so that a snapshot and the restore built from it\n * can never disagree about which paths are in scope.\n */\nexport interface HistoryOptions {\n\t/** Paths excluded from capture and from restore. */\n\tisIgnored: PathMatcher\n\t/** Paths whose live value is captured as the previous entry's instead. */\n\tisVolatile: VolatileMatcher\n\t/** Entries kept before the oldest is evicted. */\n\tmaxHistory: number\n}\n\nexport interface UndoHistory {\n\tstack: HistoryEntry[]\n\t/** Index of the entry representing the current form state. */\n\tindex: number\n\t/**\n\t * The comparable state of the persisted document, or null before anything has\n\t * been saved or loaded.\n\t *\n\t * This is what makes the \"leave without saving\" prompt truthful in both\n\t * directions. Undoing back to the *first* entry is not the same as being\n\t * unsaved: after a save, the baseline moves to wherever the document was\n\t * saved from, and stepping back past that point is a real unsaved change even\n\t * though the form looks like it did on load.\n\t *\n\t * Held as a value rather than an index on purpose. An index has to be\n\t * renumbered when the cap evicts entries and invalidated when a new edit\n\t * drops the redo tail, and recording one meant pushing a snapshot at save\n\t * time, which truncated the redo tail through the very branch redo was about\n\t * to walk into. A value needs none of that bookkeeping and lets the baseline\n\t * be recorded without touching the stack at all.\n\t */\n\tsavedComparable: ComparableState | null\n\toptions: HistoryOptions\n}\n\nexport const DEFAULT_HISTORY_OPTIONS: HistoryOptions = {\n\tisIgnored: isIgnoredPath,\n\tisVolatile: () => false,\n\tmaxHistory: MAX_HISTORY_ENTRIES,\n}\n\n/**\n * Structural deep equality for JSON-ish data (objects, arrays, primitives).\n * Key order is irrelevant; `undefined` properties equal missing properties.\n */\nexport const deepEqual = (a: unknown, b: unknown): boolean => {\n\tif (a === b) return true\n\tif (typeof a === 'number' && typeof b === 'number') return Number.isNaN(a) && Number.isNaN(b)\n\tif (a == null || b == null) return a === b\n\tif (typeof a !== 'object' || typeof b !== 'object') return false\n\tif (Array.isArray(a) !== Array.isArray(b)) return false\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((item, i) => deepEqual(item, b[i]))\n\t}\n\tconst aObj = a as Record<string, unknown>\n\tconst bObj = b as Record<string, unknown>\n\tconst keys = new Set([...Object.keys(aObj), ...Object.keys(bObj)])\n\tfor (const key of keys) {\n\t\tif (!deepEqual(aObj[key], bObj[key])) return false\n\t}\n\treturn true\n}\n\n/**\n * True for one reference of a polymorphic relationship or upload.\n *\n * Deliberately structural. The alternative, asking the schema for the field's\n * type, cannot answer for a path that two blocks declare differently, and this\n * needs no such tie-break: the shape it matches, a `relationTo` slug next to an\n * id, is what Payload writes for exactly these fields.\n */\nconst isPolymorphicRef = (value: unknown): value is { relationTo: string; value: unknown } => {\n\tif (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n\tconst ref = value as Record<string, unknown>\n\treturn (\n\t\ttypeof ref.relationTo === 'string' &&\n\t\t(typeof ref.value === 'string' || typeof ref.value === 'number')\n\t)\n}\n\n/**\n * Reduce a polymorphic reference to the reference itself.\n *\n * A polymorphic relationship hands react-select's own option objects straight\n * to form state rather than mapping them down to ids the way a single-target\n * one does (see @payloadcms/ui fields/Relationship, `dataToSet`), so the value\n * carries `label` and `allowEdit` alongside `relationTo` and `value`. Those\n * move without anyone editing anything: a label refreshes when the related\n * document is saved, `allowEdit` appears once permissions resolve, and the\n * server merge after a save replaces the whole option with the bare reference.\n * The save case is the visible one, since it appends an entry identical to the\n * one before it, which then costs an extra undo to get past.\n *\n * Payload draws the same line: its own \"did this change\" test for polymorphic\n * values compares `value` and `relationTo` and nothing else.\n */\nconst toReference = (value: unknown): unknown =>\n\tisPolymorphicRef(value) ? { relationTo: value.relationTo, value: value.value } : value\n\nconst normalizeComparableValue = (value: unknown): unknown =>\n\tArray.isArray(value) ? value.map(toReference) : toReference(value)\n\n/**\n * Reduce a field state to what counts as a user-visible edit.\n *\n * For array and blocks fields the row ids are the whole story, and `value` is\n * deliberately dropped: Payload sets it to the row count, which `rowIds` already\n * encodes, and uses it as a \"no data\" marker in a way that changes without any\n * edit. A localized blocks field with no value in the active locale is built\n * with `value: null`, and the next form-state build, triggered by an edit to\n * some unrelated field, rewrites it to `0` (arrays do the same between\n * `undefined` and `0`, see @payloadcms/ui addFieldStatePromise).\n *\n * Comparing that would report an edit nobody made. Worse, the phantom lands on\n * the capture that runs at the start of the next restore, which appends an\n * entry and truncates the redo tail, so redo silently stops working after the\n * first undo.\n */\nconst extractComparableField = (field: FormState[string]): ComparableField =>\n\tfield.rows\n\t\t? { rowIds: field.rows.map((row) => row.id) }\n\t\t: { value: normalizeComparableValue(field.value) }\n\nexport const extractComparable = (\n\tfields: FormState,\n\tisIgnored: PathMatcher = isIgnoredPath\n): ComparableState => {\n\tconst out: ComparableState = {}\n\tfor (const [path, field] of Object.entries(fields)) {\n\t\tif (isIgnored(path) || !field) continue\n\t\tout[path] = extractComparableField(field)\n\t}\n\treturn out\n}\n\nlet nextEntryId = 0\n\nexport const createSnapshot = (\n\tfields: FormState,\n\tisIgnored: PathMatcher = isIgnoredPath\n): HistoryEntry => ({\n\tid: nextEntryId++,\n\tfields: Object.fromEntries(Object.entries(fields).map(([path, field]) => [path, { ...field }])),\n\tcomparable: extractComparable(fields, isIgnored),\n})\n\nexport const createHistory = (options: Partial<HistoryOptions> = {}): UndoHistory => ({\n\tstack: [],\n\tindex: -1,\n\tsavedComparable: null,\n\toptions: { ...DEFAULT_HISTORY_OPTIONS, ...options },\n})\n\n/**\n * Record `fields` as the persisted state. Call whenever the form becomes clean:\n * a save, an autosave, a reset, the initial load.\n *\n * Reads the live form state rather than the stack, so a save made with edits\n * still inside the capture debounce is recorded accurately, and does not append\n * an entry, so it can never disturb the redo tail.\n */\nexport const markSaved = (history: UndoHistory, fields: FormState): void => {\n\thistory.savedComparable = projectComparable(history, fields)\n}\n\n/** True when the form matches the persisted document, so it can be reported clean. */\nexport const isAtSavedState = (history: UndoHistory): boolean => {\n\tif (!history.savedComparable) return false\n\tconst current = history.stack[history.index]\n\treturn current !== undefined && deepEqual(current.comparable, history.savedComparable)\n}\n\n/**\n * What a volatile path contributes to a snapshot: whatever the previous entry\n * recorded for it, or, with no entry to carry from, the live field reverted to\n * its `initialValue`, which is the persisted value and therefore a state the\n * document really was in.\n */\nconst carryForward = (\n\tpath: string,\n\tlive: FormState[string],\n\tprevious: HistoryEntry | undefined\n): { comparable: ComparableField; field: FormState[string] } => {\n\tconst carried = previous?.fields[path]\n\tif (carried) {\n\t\treturn {\n\t\t\tcomparable: previous?.comparable[path] ?? extractComparableField(carried),\n\t\t\tfield: carried,\n\t\t}\n\t}\n\tconst reverted = { ...live, value: live.initialValue }\n\treturn { comparable: extractComparableField(reverted), field: reverted }\n}\n\n/**\n * Every volatile, non-ignored path of `fields`, mapped to what it carries\n * forward from the entry the form currently shows.\n */\nconst collectCarried = (\n\thistory: UndoHistory,\n\tfields: FormState\n): Map<string, { comparable: ComparableField; field: FormState[string] }> => {\n\tconst { isIgnored, isVolatile } = history.options\n\tconst previous = history.stack[history.index]\n\tconst out = new Map<string, { comparable: ComparableField; field: FormState[string] }>()\n\tfor (const [path, field] of Object.entries(fields)) {\n\t\tif (!field || isIgnored(path) || !isVolatile(path, field)) continue\n\t\tout.set(path, carryForward(path, field, previous))\n\t}\n\treturn out\n}\n\n/**\n * The comparable state a capture of `fields` would record right now, carry\n * forward included.\n *\n * The debug overlay diffs live form state against this rather than against a\n * plain extract, so a value that can never be captured does not show up as a\n * change that is forever pending.\n */\nexport const projectComparable = (history: UndoHistory, fields: FormState): ComparableState => {\n\tconst out = extractComparable(fields, history.options.isIgnored)\n\tfor (const [path, carried] of collectCarried(history, fields)) {\n\t\tout[path] = carried.comparable\n\t}\n\treturn out\n}\n\n/**\n * Push the current form state onto the history. No-ops (and returns false) when\n * nothing user-visible changed relative to the entry at the current index,\n * which absorbs server-merge echoes after saves/restores. A real change drops\n * the redo tail, appends, and caps the stack at the history's `maxHistory`.\n */\nexport const pushSnapshot = (history: UndoHistory, fields: FormState): boolean => {\n\tconst { isIgnored, maxHistory } = history.options\n\tconst snapshot = createSnapshot(fields, isIgnored)\n\tconst current = history.stack[history.index]\n\t// Substituting here rather than at restore time is what keeps the stack\n\t// honest: an entry's comparable always matches what restoring it produces, so\n\t// a restore cannot land on a state that differs from its own entry and have\n\t// the next capture record that difference as a phantom edit, truncating the\n\t// redo tail.\n\tfor (const [path, carried] of collectCarried(history, fields)) {\n\t\tsnapshot.fields[path] = carried.field\n\t\tsnapshot.comparable[path] = carried.comparable\n\t}\n\tif (current && deepEqual(current.comparable, snapshot.comparable)) return false\n\thistory.stack.splice(history.index + 1)\n\thistory.stack.push(snapshot)\n\tif (history.stack.length > maxHistory) {\n\t\thistory.stack.splice(0, history.stack.length - maxHistory)\n\t}\n\thistory.index = history.stack.length - 1\n\treturn true\n}\n\n/** A single path-level change between two comparable states. */\nexport interface ComparableDiff {\n\tpath: string\n\tfrom: unknown\n\tto: unknown\n\t/** Set only when the path carries array/blocks rows and those rows changed. */\n\tfromRowIds?: (string | undefined)[]\n\ttoRowIds?: (string | undefined)[]\n\t/** Whether the path itself appeared or disappeared between the two states. */\n\tpresence?: 'added' | 'removed'\n}\n\n/**\n * Path-level changes from one comparable state to another, sorted by path.\n * Powers the debug overlay and makes \"what did this history entry capture?\"\n * answerable without diffing whole FormState objects by eye.\n */\nexport const diffComparable = (from: ComparableState, to: ComparableState): ComparableDiff[] => {\n\tconst out: ComparableDiff[] = []\n\tfor (const path of new Set([...Object.keys(from), ...Object.keys(to)])) {\n\t\tconst a = from[path]\n\t\tconst b = to[path]\n\t\tif (a && b && deepEqual(a.value, b.value) && deepEqual(a.rowIds, b.rowIds)) continue\n\t\tconst diff: ComparableDiff = { path, from: a?.value, to: b?.value }\n\t\tif (a?.rowIds || b?.rowIds) {\n\t\t\tdiff.fromRowIds = a?.rowIds\n\t\t\tdiff.toRowIds = b?.rowIds\n\t\t}\n\t\tif (!a) diff.presence = 'added'\n\t\telse if (!b) diff.presence = 'removed'\n\t\tout.push(diff)\n\t}\n\treturn out.sort((x, y) => x.path.localeCompare(y.path))\n}\n\nexport const canUndo = (history: UndoHistory): boolean => history.index > 0\n\nexport const canRedo = (history: UndoHistory): boolean =>\n\thistory.index >= 0 && history.index < history.stack.length - 1\n\n/** Lexical editor state values have the shape `{ root: { children: [...] } }`. */\nconst isLexicalValue = (value: unknown): boolean => {\n\tif (value == null || typeof value !== 'object') return false\n\tconst root = (value as Record<string, unknown>).root\n\treturn root != null && typeof root === 'object' && 'children' in (root as object)\n}\n\nconst cloneJson = <T>(value: T): T =>\n\tvalue == null ? value : (JSON.parse(JSON.stringify(value)) as T)\n\n/**\n * A value that means the same as `live` but is never `Object.is` to it.\n *\n * Lexical only re-initializes a mounted editor when the `initialValue` it is\n * handed is a different *reference* from the one it last saw, so restoring has\n * to hand it a new one. Cloning covers that for an editor state object, but not\n * for a rich text field that was never saved, whose initialValue is nullish: a\n * primitive cannot be cloned into a fresh identity, and `null` re-sent as\n * `null` looks unchanged.\n *\n * Undo appeared to work there because it swapped `undefined` for `null`, which\n * is an identity change once. Every restore after it kept sending `null` and\n * the editor stayed on screen holding stale content, even though the form value\n * underneath had been restored, which is why saving made the change appear.\n * Alternating between the two nullish values keeps every restore distinguishable\n * while leaving the meaning, \"this field started out empty\", untouched.\n */\nconst withNewIdentity = (live: unknown): unknown => {\n\tif (live === null) return undefined\n\tif (live === undefined) return null\n\treturn cloneJson(live)\n}\n\n/**\n * Build the FormState to dispatch via REPLACE_STATE when restoring `snapshot`.\n *\n * - `initialValue` is taken from the live state, not the snapshot, so Payload's\n * modified/\"leave without saving\" detection stays truthful after save cycles.\n * - Changed fields get `isModified: true`, which stops a stale in-flight\n * autosave response from overwriting the restored value during the server\n * form-state merge (local modified fields win in `mergeServerFormState`).\n * - Changed rich text fields additionally get a *new* `initialValue` reference:\n * the mounted Lexical editor only re-initializes from the form value when the\n * initialValue reference changes (see @payloadcms/richtext-lexical Field.tsx).\n */\nexport const buildRestoreState = (\n\tsnapshot: HistoryEntry,\n\tcurrentFields: FormState,\n\tisIgnored: PathMatcher = isIgnoredPath\n): FormState => {\n\tconst out: FormState = {}\n\tfor (const [path, snapField] of Object.entries(snapshot.fields)) {\n\t\tif (isIgnored(path)) continue\n\t\tconst cur = currentFields[path]\n\t\tconst snapComparable = snapshot.comparable[path] ?? extractComparableField(snapField)\n\t\tconst changed = !cur || !deepEqual(snapComparable, extractComparableField(cur))\n\t\tconst restored: FormState[string] = { ...snapField }\n\t\tif (cur) restored.initialValue = cur.initialValue\n\t\tif (changed) {\n\t\t\trestored.isModified = true\n\t\t\tif (isLexicalValue(snapField.value) || isLexicalValue(cur?.value)) {\n\t\t\t\trestored.initialValue = withNewIdentity(cur?.initialValue)\n\t\t\t}\n\t\t}\n\t\tout[path] = restored\n\t}\n\t// Ignored paths pass through untouched from the live state: undo must\n\t// neither revert them nor drop them from the replaced state.\n\tfor (const [path, curField] of Object.entries(currentFields)) {\n\t\tif (isIgnored(path)) out[path] = curField\n\t}\n\treturn out\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"historyCore.js","names":[],"sources":["../../src/history/historyCore.ts"],"sourcesContent":["import type { FormState } from 'payload'\n\nimport { createPathMatcher, type PathMatcher } from './pathPatterns'\n\n/**\n * Pure, React-free core of the admin undo/redo feature.\n *\n * The admin form keeps its state in a flat `FormState` map (path → FieldState).\n * We snapshot that map on (debounced) user edits and restore old snapshots via\n * the form's `REPLACE_STATE` reducer action. Field states carry non-serializable\n * members (React nodes in `customComponents`, `validate` functions), so snapshots\n * shallow-copy each field state and keep those members by reference: only the\n * top level is protected against later mutation, which matches how the form\n * reducer itself treats field states (immutable-style replacement).\n */\n\n/**\n * Payload's own fields, which no editor edits directly and undo must never\n * touch. They change on save, autosave, publish and token refresh without user\n * interaction, so treating them as edits would create \"phantom\" history entries\n * where undo appears to do nothing, and restoring a stale value could revert a\n * publish (`_status`) or corrupt auth state (`sessions`, `salt`, `hash`).\n *\n * Deliberately limited to fields Payload itself injects. Derived fields that a\n * plugin or project adds are user-facing (a project may well have its own\n * `pathname`), so excluding those is the host's call through `ignorePaths`\n * rather than a default baked in here.\n *\n * Each entry ignores its whole subtree, which is what the pattern matcher does\n * for a pattern that runs out before the path does.\n */\nexport const DEFAULT_IGNORED_PATHS = [\n\t'_status',\n\t'createdAt',\n\t'hash',\n\t'lockUntil',\n\t'loginAttempts',\n\t'resetPasswordExpiration',\n\t'resetPasswordToken',\n\t'salt',\n\t'sessions',\n\t'updatedAt',\n] as const\n\n/** True for paths the undo history ignores entirely (see DEFAULT_IGNORED_PATHS). */\nexport const isIgnoredPath: PathMatcher = createPathMatcher(DEFAULT_IGNORED_PATHS)\n\nexport interface ComparableField {\n\t/** Absent for array and blocks fields, whose `value` carries no information (see extractComparableField). */\n\tvalue?: unknown\n\t/** Row ids in order, which captures array/blocks row additions, deletions and moves. */\n\trowIds?: (string | undefined)[]\n}\n\n/** The slice of form state that counts as a user-visible edit. */\nexport type ComparableState = Record<string, ComparableField>\n\nexport interface HistoryEntry {\n\t/**\n\t * Monotonic id, stable across the stack shifting when the cap evicts the\n\t * oldest entries. Positional indexes are not stable for that reason, so\n\t * anything keyed per entry (React keys, debug UI expansion) uses this.\n\t */\n\tid: number\n\tfields: FormState\n\tcomparable: ComparableState\n}\n\nexport const MAX_HISTORY_ENTRIES = 50\n\n/**\n * True for a field whose current value the history cannot put back.\n *\n * Payload's JSON field is the case this exists for. While its text does not\n * parse, the field writes the raw editor text into form state as a string, and\n * the editor is then rendered from `JSON.stringify(value)`, so dispatching that\n * string back would show it double-encoded rather than as the text the editor\n * had. No value we could dispatch reproduces broken text, so rather than record\n * a state it cannot honour, a capture carries the last value it can (see\n * pushSnapshot).\n */\nexport type VolatileMatcher = (path: string, field: FormState[string]) => boolean\n\n/**\n * Per-history settings. They live on the history object rather than being\n * threaded through every call so that a snapshot and the restore built from it\n * can never disagree about which paths are in scope.\n */\nexport interface HistoryOptions {\n\t/** Paths excluded from capture and from restore. */\n\tisIgnored: PathMatcher\n\t/** Paths whose live value is captured as the previous entry's instead. */\n\tisVolatile: VolatileMatcher\n\t/** Entries kept before the oldest is evicted. */\n\tmaxHistory: number\n}\n\nexport interface UndoHistory {\n\tstack: HistoryEntry[]\n\t/** Index of the entry representing the current form state. */\n\tindex: number\n\t/**\n\t * The comparable state of the persisted document, or null before anything has\n\t * been saved or loaded.\n\t *\n\t * This is what makes the \"leave without saving\" prompt truthful in both\n\t * directions. Undoing back to the *first* entry is not the same as being\n\t * unsaved: after a save, the baseline moves to wherever the document was\n\t * saved from, and stepping back past that point is a real unsaved change even\n\t * though the form looks like it did on load.\n\t *\n\t * Held as a value rather than an index on purpose. An index has to be\n\t * renumbered when the cap evicts entries and invalidated when a new edit\n\t * drops the redo tail, and recording one meant pushing a snapshot at save\n\t * time, which truncated the redo tail through the very branch redo was about\n\t * to walk into. A value needs none of that bookkeeping and lets the baseline\n\t * be recorded without touching the stack at all.\n\t */\n\tsavedComparable: ComparableState | null\n\toptions: HistoryOptions\n}\n\nexport const DEFAULT_HISTORY_OPTIONS: HistoryOptions = {\n\tisIgnored: isIgnoredPath,\n\tisVolatile: () => false,\n\tmaxHistory: MAX_HISTORY_ENTRIES,\n}\n\n/**\n * Structural deep equality for JSON-ish data (objects, arrays, primitives).\n * Key order is irrelevant; `undefined` properties equal missing properties.\n */\nexport const deepEqual = (a: unknown, b: unknown): boolean => {\n\tif (a === b) return true\n\tif (typeof a === 'number' && typeof b === 'number') return Number.isNaN(a) && Number.isNaN(b)\n\tif (a == null || b == null) return a === b\n\tif (typeof a !== 'object' || typeof b !== 'object') return false\n\tif (Array.isArray(a) !== Array.isArray(b)) return false\n\tif (Array.isArray(a) && Array.isArray(b)) {\n\t\tif (a.length !== b.length) return false\n\t\treturn a.every((item, i) => deepEqual(item, b[i]))\n\t}\n\tconst aObj = a as Record<string, unknown>\n\tconst bObj = b as Record<string, unknown>\n\tconst keys = new Set([...Object.keys(aObj), ...Object.keys(bObj)])\n\tfor (const key of keys) {\n\t\tif (!deepEqual(aObj[key], bObj[key])) return false\n\t}\n\treturn true\n}\n\n/**\n * True for one reference of a polymorphic relationship or upload.\n *\n * Deliberately structural. The alternative, asking the schema for the field's\n * type, cannot answer for a path that two blocks declare differently, and this\n * needs no such tie-break: the shape it matches, a `relationTo` slug next to an\n * id, is what Payload writes for exactly these fields.\n */\nconst isPolymorphicRef = (value: unknown): value is { relationTo: string; value: unknown } => {\n\tif (typeof value !== 'object' || value === null || Array.isArray(value)) return false\n\tconst ref = value as Record<string, unknown>\n\treturn (\n\t\ttypeof ref.relationTo === 'string' &&\n\t\t(typeof ref.value === 'string' || typeof ref.value === 'number')\n\t)\n}\n\n/**\n * Reduce a polymorphic reference to the reference itself.\n *\n * A polymorphic relationship hands react-select's own option objects straight\n * to form state rather than mapping them down to ids the way a single-target\n * one does (see @payloadcms/ui fields/Relationship, `dataToSet`), so the value\n * carries `label` and `allowEdit` alongside `relationTo` and `value`. Those\n * move without anyone editing anything: a label refreshes when the related\n * document is saved, `allowEdit` appears once permissions resolve, and the\n * server merge after a save replaces the whole option with the bare reference.\n * The save case is the visible one, since it appends an entry identical to the\n * one before it, which then costs an extra undo to get past.\n *\n * Payload draws the same line: its own \"did this change\" test for polymorphic\n * values compares `value` and `relationTo` and nothing else.\n */\nconst toReference = (value: unknown): unknown =>\n\tisPolymorphicRef(value) ? { relationTo: value.relationTo, value: value.value } : value\n\nconst normalizeComparableValue = (value: unknown): unknown =>\n\tArray.isArray(value) ? value.map(toReference) : toReference(value)\n\n/**\n * Reduce a field state to what counts as a user-visible edit.\n *\n * For array and blocks fields the row ids are the whole story, and `value` is\n * deliberately dropped: Payload sets it to the row count, which `rowIds` already\n * encodes, and uses it as a \"no data\" marker in a way that changes without any\n * edit. A localized blocks field with no value in the active locale is built\n * with `value: null`, and the next form-state build, triggered by an edit to\n * some unrelated field, rewrites it to `0` (arrays do the same between\n * `undefined` and `0`, see @payloadcms/ui addFieldStatePromise).\n *\n * Comparing that would report an edit nobody made. Worse, the phantom lands on\n * the capture that runs at the start of the next restore, which appends an\n * entry and truncates the redo tail, so redo silently stops working after the\n * first undo.\n */\nconst extractComparableField = (field: FormState[string]): ComparableField =>\n\tfield.rows\n\t\t? { rowIds: field.rows.map((row) => row.id) }\n\t\t: { value: normalizeComparableValue(field.value) }\n\nexport const extractComparable = (\n\tfields: FormState,\n\tisIgnored: PathMatcher = isIgnoredPath\n): ComparableState => {\n\tconst out: ComparableState = {}\n\tfor (const [path, field] of Object.entries(fields)) {\n\t\tif (isIgnored(path) || !field) continue\n\t\tout[path] = extractComparableField(field)\n\t}\n\treturn out\n}\n\nlet nextEntryId = 0\n\nexport const createSnapshot = (\n\tfields: FormState,\n\tisIgnored: PathMatcher = isIgnoredPath\n): HistoryEntry => ({\n\tid: nextEntryId++,\n\tfields: Object.fromEntries(Object.entries(fields).map(([path, field]) => [path, { ...field }])),\n\tcomparable: extractComparable(fields, isIgnored),\n})\n\nexport const createHistory = (options: Partial<HistoryOptions> = {}): UndoHistory => ({\n\tstack: [],\n\tindex: -1,\n\tsavedComparable: null,\n\toptions: { ...DEFAULT_HISTORY_OPTIONS, ...options },\n})\n\n/**\n * Record `fields` as the persisted state. Call whenever the form becomes clean:\n * a save, an autosave, a reset, the initial load.\n *\n * Reads the live form state rather than the stack, so a save made with edits\n * still inside the capture debounce is recorded accurately, and does not append\n * an entry, so it can never disturb the redo tail.\n */\nexport const markSaved = (history: UndoHistory, fields: FormState): void => {\n\thistory.savedComparable = projectComparable(history, fields)\n}\n\n/** True when the form matches the persisted document, so it can be reported clean. */\nexport const isAtSavedState = (history: UndoHistory): boolean => {\n\tif (!history.savedComparable) return false\n\tconst current = history.stack[history.index]\n\treturn current !== undefined && deepEqual(current.comparable, history.savedComparable)\n}\n\n/**\n * What a volatile path contributes to a snapshot: whatever the previous entry\n * recorded for it, or, with no entry to carry from, the live field reverted to\n * its `initialValue`, which is the persisted value and therefore a state the\n * document really was in.\n */\nconst carryForward = (\n\tpath: string,\n\tlive: FormState[string],\n\tprevious: HistoryEntry | undefined\n): { comparable: ComparableField; field: FormState[string] } => {\n\tconst carried = previous?.fields[path]\n\tif (carried) {\n\t\treturn {\n\t\t\tcomparable: previous?.comparable[path] ?? extractComparableField(carried),\n\t\t\tfield: carried,\n\t\t}\n\t}\n\tconst reverted = { ...live, value: live.initialValue }\n\treturn { comparable: extractComparableField(reverted), field: reverted }\n}\n\n/**\n * Every volatile, non-ignored path of `fields`, mapped to what it carries\n * forward from the entry the form currently shows.\n */\nconst collectCarried = (\n\thistory: UndoHistory,\n\tfields: FormState\n): Map<string, { comparable: ComparableField; field: FormState[string] }> => {\n\tconst { isIgnored, isVolatile } = history.options\n\tconst previous = history.stack[history.index]\n\tconst out = new Map<string, { comparable: ComparableField; field: FormState[string] }>()\n\tfor (const [path, field] of Object.entries(fields)) {\n\t\tif (!field || isIgnored(path) || !isVolatile(path, field)) continue\n\t\tout.set(path, carryForward(path, field, previous))\n\t}\n\treturn out\n}\n\n/**\n * The comparable state a capture of `fields` would record right now, carry\n * forward included.\n *\n * The debug overlay diffs live form state against this rather than against a\n * plain extract, so a value that can never be captured does not show up as a\n * change that is forever pending.\n */\nexport const projectComparable = (history: UndoHistory, fields: FormState): ComparableState => {\n\tconst out = extractComparable(fields, history.options.isIgnored)\n\tfor (const [path, carried] of collectCarried(history, fields)) {\n\t\tout[path] = carried.comparable\n\t}\n\treturn out\n}\n\n/**\n * True when `to` is `from` plus paths that did not exist before, with nothing\n * that did exist having changed.\n *\n * This is the shape of Payload finishing a job rather than of an edit. Adding a\n * row dispatches `ADD_ROW` with a blank row and leaves the real state to the\n * debounced form-state request that follows (see @payloadcms/ui Form\n * `addFieldRow`), so the row's own fields arrive as a second wave, milliseconds\n * to seconds later depending on how big the config is. Every user action that\n * adds paths brings something else with it: adding a row changes the row ids,\n * revealing a conditional field changes the value that controls it. A wave of\n * pure additions has no such companion, and treating it as an edit costs a\n * second undo to get past a single click.\n */\nconst addsPathsOnly = (from: ComparableState, to: ComparableState): boolean => {\n\tlet added = false\n\tfor (const path of Object.keys(to)) {\n\t\tif (!(path in from)) added = true\n\t}\n\tif (!added) return false\n\treturn Object.keys(from).every((path) => deepEqual(from[path], to[path]))\n}\n\n/**\n * Push the current form state onto the history. No-ops (and returns false) when\n * nothing user-visible changed relative to the entry at the current index,\n * which absorbs server-merge echoes after saves/restores. A real change drops\n * the redo tail, appends, and caps the stack at the history's `maxHistory`.\n *\n * A wave of pure path additions is folded into the current entry instead of\n * appended (see addsPathsOnly). Folded rather than dropped, because an entry\n * restores exactly the paths it holds: leaving the new ones out would make\n * stepping back onto that entry delete the fields Payload had just filled in.\n */\nexport const pushSnapshot = (history: UndoHistory, fields: FormState): boolean => {\n\tconst { isIgnored, maxHistory } = history.options\n\tconst snapshot = createSnapshot(fields, isIgnored)\n\tconst current = history.stack[history.index]\n\t// Substituting here rather than at restore time is what keeps the stack\n\t// honest: an entry's comparable always matches what restoring it produces, so\n\t// a restore cannot land on a state that differs from its own entry and have\n\t// the next capture record that difference as a phantom edit, truncating the\n\t// redo tail.\n\tfor (const [path, carried] of collectCarried(history, fields)) {\n\t\tsnapshot.fields[path] = carried.field\n\t\tsnapshot.comparable[path] = carried.comparable\n\t}\n\tif (current) {\n\t\tif (deepEqual(current.comparable, snapshot.comparable)) return false\n\t\tif (addsPathsOnly(current.comparable, snapshot.comparable)) {\n\t\t\tcurrent.fields = snapshot.fields\n\t\t\tcurrent.comparable = snapshot.comparable\n\t\t\treturn false\n\t\t}\n\t}\n\thistory.stack.splice(history.index + 1)\n\thistory.stack.push(snapshot)\n\tif (history.stack.length > maxHistory) {\n\t\thistory.stack.splice(0, history.stack.length - maxHistory)\n\t}\n\thistory.index = history.stack.length - 1\n\treturn true\n}\n\n/** A single path-level change between two comparable states. */\nexport interface ComparableDiff {\n\tpath: string\n\tfrom: unknown\n\tto: unknown\n\t/** Set only when the path carries array/blocks rows and those rows changed. */\n\tfromRowIds?: (string | undefined)[]\n\ttoRowIds?: (string | undefined)[]\n\t/** Whether the path itself appeared or disappeared between the two states. */\n\tpresence?: 'added' | 'removed'\n}\n\n/**\n * Path-level changes from one comparable state to another, sorted by path.\n * Powers the debug overlay and makes \"what did this history entry capture?\"\n * answerable without diffing whole FormState objects by eye.\n */\nexport const diffComparable = (from: ComparableState, to: ComparableState): ComparableDiff[] => {\n\tconst out: ComparableDiff[] = []\n\tfor (const path of new Set([...Object.keys(from), ...Object.keys(to)])) {\n\t\tconst a = from[path]\n\t\tconst b = to[path]\n\t\tif (a && b && deepEqual(a.value, b.value) && deepEqual(a.rowIds, b.rowIds)) continue\n\t\tconst diff: ComparableDiff = { path, from: a?.value, to: b?.value }\n\t\tif (a?.rowIds || b?.rowIds) {\n\t\t\tdiff.fromRowIds = a?.rowIds\n\t\t\tdiff.toRowIds = b?.rowIds\n\t\t}\n\t\tif (!a) diff.presence = 'added'\n\t\telse if (!b) diff.presence = 'removed'\n\t\tout.push(diff)\n\t}\n\treturn out.sort((x, y) => x.path.localeCompare(y.path))\n}\n\nexport const canUndo = (history: UndoHistory): boolean => history.index > 0\n\nexport const canRedo = (history: UndoHistory): boolean =>\n\thistory.index >= 0 && history.index < history.stack.length - 1\n\n/** Lexical editor state values have the shape `{ root: { children: [...] } }`. */\nconst isLexicalValue = (value: unknown): boolean => {\n\tif (value == null || typeof value !== 'object') return false\n\tconst root = (value as Record<string, unknown>).root\n\treturn root != null && typeof root === 'object' && 'children' in (root as object)\n}\n\nconst cloneJson = <T>(value: T): T =>\n\tvalue == null ? value : (JSON.parse(JSON.stringify(value)) as T)\n\n/**\n * A value that means the same as `live` but is never `Object.is` to it.\n *\n * Lexical only re-initializes a mounted editor when the `initialValue` it is\n * handed is a different *reference* from the one it last saw, so restoring has\n * to hand it a new one. Cloning covers that for an editor state object, but not\n * for a rich text field that was never saved, whose initialValue is nullish: a\n * primitive cannot be cloned into a fresh identity, and `null` re-sent as\n * `null` looks unchanged.\n *\n * Undo appeared to work there because it swapped `undefined` for `null`, which\n * is an identity change once. Every restore after it kept sending `null` and\n * the editor stayed on screen holding stale content, even though the form value\n * underneath had been restored, which is why saving made the change appear.\n * Alternating between the two nullish values keeps every restore distinguishable\n * while leaving the meaning, \"this field started out empty\", untouched.\n */\nconst withNewIdentity = (live: unknown): unknown => {\n\tif (live === null) return undefined\n\tif (live === undefined) return null\n\treturn cloneJson(live)\n}\n\n/**\n * Build the FormState to dispatch via REPLACE_STATE when restoring `snapshot`.\n *\n * - `initialValue` is taken from the live state, not the snapshot, so Payload's\n * modified/\"leave without saving\" detection stays truthful after save cycles.\n * - Changed fields get `isModified: true`, which stops a stale in-flight\n * autosave response from overwriting the restored value during the server\n * form-state merge (local modified fields win in `mergeServerFormState`).\n * - Changed rich text fields additionally get a *new* `initialValue` reference:\n * the mounted Lexical editor only re-initializes from the form value when the\n * initialValue reference changes (see @payloadcms/richtext-lexical Field.tsx).\n */\nexport const buildRestoreState = (\n\tsnapshot: HistoryEntry,\n\tcurrentFields: FormState,\n\tisIgnored: PathMatcher = isIgnoredPath\n): FormState => {\n\tconst out: FormState = {}\n\tfor (const [path, snapField] of Object.entries(snapshot.fields)) {\n\t\tif (isIgnored(path)) continue\n\t\tconst cur = currentFields[path]\n\t\tconst snapComparable = snapshot.comparable[path] ?? extractComparableField(snapField)\n\t\tconst changed = !cur || !deepEqual(snapComparable, extractComparableField(cur))\n\t\tconst restored: FormState[string] = { ...snapField }\n\t\tif (cur) restored.initialValue = cur.initialValue\n\t\tif (changed) {\n\t\t\trestored.isModified = true\n\t\t\tif (isLexicalValue(snapField.value) || isLexicalValue(cur?.value)) {\n\t\t\t\trestored.initialValue = withNewIdentity(cur?.initialValue)\n\t\t\t}\n\t\t}\n\t\tout[path] = restored\n\t}\n\t// Ignored paths pass through untouched from the live state: undo must\n\t// neither revert them nor drop them from the replaced state.\n\tfor (const [path, curField] of Object.entries(currentFields)) {\n\t\tif (isIgnored(path)) out[path] = curField\n\t}\n\treturn out\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/ar.ts
|
|
3
|
+
const ar = {
|
|
4
|
+
[keys.undo]: "تراجع",
|
|
5
|
+
[keys.redo]: "إعادة",
|
|
6
|
+
[keys.debug]: "سجل التراجع",
|
|
7
|
+
[keys.debugTooltip]: "فحص سجل التراجع",
|
|
8
|
+
[keys.debugTitle]: "سجل التراجع",
|
|
9
|
+
[keys.debugEmpty]: "لم يُسجَّل أي سجل بعد.",
|
|
10
|
+
[keys.debugClose]: "إغلاق",
|
|
11
|
+
[keys.debugPending]: "قيد الانتظار (غير مسجل)",
|
|
12
|
+
[keys.debugOriginal]: "الحالة الأصلية",
|
|
13
|
+
[keys.debugCopy]: "نسخ JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ar };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=ar.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ar.js","names":[],"sources":["../../src/translations/ar.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const ar: Record<TranslationKey, string> = {\n\t[keys.undo]: 'تراجع',\n\t[keys.redo]: 'إعادة',\n\t[keys.debug]: 'سجل التراجع',\n\t[keys.debugTooltip]: 'فحص سجل التراجع',\n\t[keys.debugTitle]: 'سجل التراجع',\n\t[keys.debugEmpty]: 'لم يُسجَّل أي سجل بعد.',\n\t[keys.debugClose]: 'إغلاق',\n\t[keys.debugPending]: 'قيد الانتظار (غير مسجل)',\n\t[keys.debugOriginal]: 'الحالة الأصلية',\n\t[keys.debugCopy]: 'نسخ JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/es.ts
|
|
3
|
+
const es = {
|
|
4
|
+
[keys.undo]: "Deshacer",
|
|
5
|
+
[keys.redo]: "Rehacer",
|
|
6
|
+
[keys.debug]: "Historial de deshacer",
|
|
7
|
+
[keys.debugTooltip]: "Inspeccionar el historial de deshacer",
|
|
8
|
+
[keys.debugTitle]: "Historial de deshacer",
|
|
9
|
+
[keys.debugEmpty]: "Todavía no se ha registrado ningún historial.",
|
|
10
|
+
[keys.debugClose]: "Cerrar",
|
|
11
|
+
[keys.debugPending]: "Pendiente (sin registrar)",
|
|
12
|
+
[keys.debugOriginal]: "Estado original",
|
|
13
|
+
[keys.debugCopy]: "Copiar JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { es };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=es.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"es.js","names":[],"sources":["../../src/translations/es.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const es: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Deshacer',\n\t[keys.redo]: 'Rehacer',\n\t[keys.debug]: 'Historial de deshacer',\n\t[keys.debugTooltip]: 'Inspeccionar el historial de deshacer',\n\t[keys.debugTitle]: 'Historial de deshacer',\n\t[keys.debugEmpty]: 'Todavía no se ha registrado ningún historial.',\n\t[keys.debugClose]: 'Cerrar',\n\t[keys.debugPending]: 'Pendiente (sin registrar)',\n\t[keys.debugOriginal]: 'Estado original',\n\t[keys.debugCopy]: 'Copiar JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/fr.ts
|
|
3
|
+
const fr = {
|
|
4
|
+
[keys.undo]: "Annuler",
|
|
5
|
+
[keys.redo]: "Rétablir",
|
|
6
|
+
[keys.debug]: "Historique des annulations",
|
|
7
|
+
[keys.debugTooltip]: "Inspecter l'historique des annulations",
|
|
8
|
+
[keys.debugTitle]: "Historique des annulations",
|
|
9
|
+
[keys.debugEmpty]: "Aucun historique enregistré pour le moment.",
|
|
10
|
+
[keys.debugClose]: "Fermer",
|
|
11
|
+
[keys.debugPending]: "En attente (non enregistré)",
|
|
12
|
+
[keys.debugOriginal]: "État initial",
|
|
13
|
+
[keys.debugCopy]: "Copier le JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { fr };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=fr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fr.js","names":[],"sources":["../../src/translations/fr.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const fr: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Annuler',\n\t[keys.redo]: 'Rétablir',\n\t[keys.debug]: 'Historique des annulations',\n\t[keys.debugTooltip]: \"Inspecter l'historique des annulations\",\n\t[keys.debugTitle]: 'Historique des annulations',\n\t[keys.debugEmpty]: 'Aucun historique enregistré pour le moment.',\n\t[keys.debugClose]: 'Fermer',\n\t[keys.debugPending]: 'En attente (non enregistré)',\n\t[keys.debugOriginal]: 'État initial',\n\t[keys.debugCopy]: 'Copier le JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/id.ts
|
|
3
|
+
const id = {
|
|
4
|
+
[keys.undo]: "Urungkan",
|
|
5
|
+
[keys.redo]: "Ulangi",
|
|
6
|
+
[keys.debug]: "Riwayat urungkan",
|
|
7
|
+
[keys.debugTooltip]: "Periksa riwayat urungkan",
|
|
8
|
+
[keys.debugTitle]: "Riwayat urungkan",
|
|
9
|
+
[keys.debugEmpty]: "Belum ada riwayat yang direkam.",
|
|
10
|
+
[keys.debugClose]: "Tutup",
|
|
11
|
+
[keys.debugPending]: "Tertunda (belum direkam)",
|
|
12
|
+
[keys.debugOriginal]: "Kondisi awal",
|
|
13
|
+
[keys.debugCopy]: "Salin JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { id };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=id.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"id.js","names":[],"sources":["../../src/translations/id.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const id: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Urungkan',\n\t[keys.redo]: 'Ulangi',\n\t[keys.debug]: 'Riwayat urungkan',\n\t[keys.debugTooltip]: 'Periksa riwayat urungkan',\n\t[keys.debugTitle]: 'Riwayat urungkan',\n\t[keys.debugEmpty]: 'Belum ada riwayat yang direkam.',\n\t[keys.debugClose]: 'Tutup',\n\t[keys.debugPending]: 'Tertunda (belum direkam)',\n\t[keys.debugOriginal]: 'Kondisi awal',\n\t[keys.debugCopy]: 'Salin JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -7,8 +7,17 @@ type TranslationsOption = {
|
|
|
7
7
|
};
|
|
8
8
|
/** Per-locale messages merged into `config.i18n.translations`. */
|
|
9
9
|
declare const translations: {
|
|
10
|
-
|
|
10
|
+
ar: Record<string, Record<string, string>>;
|
|
11
11
|
de: Record<string, Record<string, string>>;
|
|
12
|
+
en: Record<string, Record<string, string>>;
|
|
13
|
+
es: Record<string, Record<string, string>>;
|
|
14
|
+
fr: Record<string, Record<string, string>>;
|
|
15
|
+
id: Record<string, Record<string, string>>;
|
|
16
|
+
ko: Record<string, Record<string, string>>;
|
|
17
|
+
pt: Record<string, Record<string, string>>;
|
|
18
|
+
ru: Record<string, Record<string, string>>;
|
|
19
|
+
uk: Record<string, Record<string, string>>;
|
|
20
|
+
zh: Record<string, Record<string, string>>;
|
|
12
21
|
};
|
|
13
22
|
//#endregion
|
|
14
23
|
export { TranslationsOption, translations };
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import "./keys.js";
|
|
2
|
+
import { ar } from "./ar.js";
|
|
2
3
|
import { de } from "./de.js";
|
|
3
4
|
import { en } from "./en.js";
|
|
5
|
+
import { es } from "./es.js";
|
|
6
|
+
import { fr } from "./fr.js";
|
|
7
|
+
import { id } from "./id.js";
|
|
8
|
+
import { ko } from "./ko.js";
|
|
9
|
+
import { pt } from "./pt.js";
|
|
10
|
+
import { ru } from "./ru.js";
|
|
11
|
+
import { uk } from "./uk.js";
|
|
12
|
+
import { zh } from "./zh.js";
|
|
4
13
|
//#region src/translations/index.ts
|
|
5
14
|
/**
|
|
6
15
|
* Flat `undoRedo:foo` entries to the nested `{ undoRedo: { foo } }`
|
|
@@ -21,8 +30,17 @@ const toNested = (flat) => {
|
|
|
21
30
|
};
|
|
22
31
|
/** Per-locale messages merged into `config.i18n.translations`. */
|
|
23
32
|
const translations = {
|
|
33
|
+
ar: toNested(ar),
|
|
34
|
+
de: toNested(de),
|
|
24
35
|
en: toNested(en),
|
|
25
|
-
|
|
36
|
+
es: toNested(es),
|
|
37
|
+
fr: toNested(fr),
|
|
38
|
+
id: toNested(id),
|
|
39
|
+
ko: toNested(ko),
|
|
40
|
+
pt: toNested(pt),
|
|
41
|
+
ru: toNested(ru),
|
|
42
|
+
uk: toNested(uk),
|
|
43
|
+
zh: toNested(zh)
|
|
26
44
|
};
|
|
27
45
|
//#endregion
|
|
28
46
|
export { toNested, translations };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/translations/index.ts"],"sourcesContent":["import { de } from './de'\nimport { en } from './en'\nimport type { TranslationKey } from './keys'\n\nexport type { TranslationKey } from './keys'\nexport { keys } from './keys'\n\n/** Per-locale string overrides keyed by this plugin's typed translation keys. */\nexport type TranslationsOption = {\n\t[locale: string]: Partial<Record<TranslationKey, string>>\n}\n\n/**\n * Flat `undoRedo:foo` entries to the nested `{ undoRedo: { foo } }`\n * shape Payload resolves `t('undoRedo:foo')` against (it splits on `:`).\n * Undefined values are skipped so `Partial` override maps pass through.\n */\nexport const toNested = (flat: {\n\t[key: string]: string | undefined\n}): Record<string, Record<string, string>> => {\n\tconst out: Record<string, Record<string, string>> = {}\n\tfor (const [fullKey, value] of Object.entries(flat)) {\n\t\tif (typeof value !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst separator = fullKey.indexOf(':')\n\t\tconst namespace = fullKey.slice(0, separator)\n\t\tconst bucket = out[namespace] ?? {}\n\t\tbucket[fullKey.slice(separator + 1)] = value\n\t\tout[namespace] = bucket\n\t}\n\treturn out\n}\n\n/** Per-locale messages merged into `config.i18n.translations`. */\nexport const translations = {\n\
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/translations/index.ts"],"sourcesContent":["import { ar } from './ar'\nimport { de } from './de'\nimport { en } from './en'\nimport { es } from './es'\nimport { fr } from './fr'\nimport { id } from './id'\nimport type { TranslationKey } from './keys'\nimport { ko } from './ko'\nimport { pt } from './pt'\nimport { ru } from './ru'\nimport { uk } from './uk'\nimport { zh } from './zh'\n\nexport type { TranslationKey } from './keys'\nexport { keys } from './keys'\n\n/** Per-locale string overrides keyed by this plugin's typed translation keys. */\nexport type TranslationsOption = {\n\t[locale: string]: Partial<Record<TranslationKey, string>>\n}\n\n/**\n * Flat `undoRedo:foo` entries to the nested `{ undoRedo: { foo } }`\n * shape Payload resolves `t('undoRedo:foo')` against (it splits on `:`).\n * Undefined values are skipped so `Partial` override maps pass through.\n */\nexport const toNested = (flat: {\n\t[key: string]: string | undefined\n}): Record<string, Record<string, string>> => {\n\tconst out: Record<string, Record<string, string>> = {}\n\tfor (const [fullKey, value] of Object.entries(flat)) {\n\t\tif (typeof value !== 'string') {\n\t\t\tcontinue\n\t\t}\n\t\tconst separator = fullKey.indexOf(':')\n\t\tconst namespace = fullKey.slice(0, separator)\n\t\tconst bucket = out[namespace] ?? {}\n\t\tbucket[fullKey.slice(separator + 1)] = value\n\t\tout[namespace] = bucket\n\t}\n\treturn out\n}\n\n/** Per-locale messages merged into `config.i18n.translations`. */\nexport const translations = {\n\tar: toNested(ar),\n\tde: toNested(de),\n\ten: toNested(en),\n\tes: toNested(es),\n\tfr: toNested(fr),\n\tid: toNested(id),\n\tko: toNested(ko),\n\tpt: toNested(pt),\n\tru: toNested(ru),\n\tuk: toNested(uk),\n\tzh: toNested(zh),\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AA0BA,MAAa,YAAY,SAEqB;CAC7C,MAAM,MAA8C,CAAC;CACrD,KAAK,MAAM,CAAC,SAAS,UAAU,OAAO,QAAQ,IAAI,GAAG;EACpD,IAAI,OAAO,UAAU,UACpB;EAED,MAAM,YAAY,QAAQ,QAAQ,GAAG;EACrC,MAAM,YAAY,QAAQ,MAAM,GAAG,SAAS;EAC5C,MAAM,SAAS,IAAI,cAAc,CAAC;EAClC,OAAO,QAAQ,MAAM,YAAY,CAAC,KAAK;EACvC,IAAI,aAAa;CAClB;CACA,OAAO;AACR;;AAGA,MAAa,eAAe;CAC3B,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;CACf,IAAI,SAAS,EAAE;AAChB"}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Typed translation keys. Lookups must go through these constants, not string
|
|
4
4
|
* literals (enforced by requireI18nKeysTyped.grit). Every key here must have a
|
|
5
|
-
* value in every locale
|
|
5
|
+
* value in every locale file, or it is a type error.
|
|
6
6
|
*/
|
|
7
7
|
declare const keys: {
|
|
8
8
|
readonly undo: "undoRedo:undo";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Typed translation keys. Lookups must go through these constants, not string
|
|
4
4
|
* literals (enforced by requireI18nKeysTyped.grit). Every key here must have a
|
|
5
|
-
* value in every locale
|
|
5
|
+
* value in every locale file, or it is a type error.
|
|
6
6
|
*/
|
|
7
7
|
const keys = {
|
|
8
8
|
undo: "undoRedo:undo",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.js","names":[],"sources":["../../src/translations/keys.ts"],"sourcesContent":["/**\n * Typed translation keys. Lookups must go through these constants, not string\n * literals (enforced by requireI18nKeysTyped.grit). Every key here must have a\n * value in every locale
|
|
1
|
+
{"version":3,"file":"keys.js","names":[],"sources":["../../src/translations/keys.ts"],"sourcesContent":["/**\n * Typed translation keys. Lookups must go through these constants, not string\n * literals (enforced by requireI18nKeysTyped.grit). Every key here must have a\n * value in every locale file, or it is a type error.\n */\nexport const keys = {\n\tundo: 'undoRedo:undo',\n\tredo: 'undoRedo:redo',\n\tdebug: 'undoRedo:debug',\n\tdebugTooltip: 'undoRedo:debugTooltip',\n\tdebugTitle: 'undoRedo:debugTitle',\n\tdebugEmpty: 'undoRedo:debugEmpty',\n\tdebugClose: 'undoRedo:debugClose',\n\tdebugPending: 'undoRedo:debugPending',\n\tdebugOriginal: 'undoRedo:debugOriginal',\n\tdebugCopy: 'undoRedo:debugCopy',\n} as const\n\nexport type TranslationKey = (typeof keys)[keyof typeof keys]\n"],"mappings":";;;;;;AAKA,MAAa,OAAO;CACnB,MAAM;CACN,MAAM;CACN,OAAO;CACP,cAAc;CACd,YAAY;CACZ,YAAY;CACZ,YAAY;CACZ,cAAc;CACd,eAAe;CACf,WAAW;AACZ"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/ko.ts
|
|
3
|
+
const ko = {
|
|
4
|
+
[keys.undo]: "실행 취소",
|
|
5
|
+
[keys.redo]: "다시 실행",
|
|
6
|
+
[keys.debug]: "실행 취소 기록",
|
|
7
|
+
[keys.debugTooltip]: "실행 취소 기록 살펴보기",
|
|
8
|
+
[keys.debugTitle]: "실행 취소 기록",
|
|
9
|
+
[keys.debugEmpty]: "아직 기록된 내역이 없습니다.",
|
|
10
|
+
[keys.debugClose]: "닫기",
|
|
11
|
+
[keys.debugPending]: "대기 중 (기록되지 않음)",
|
|
12
|
+
[keys.debugOriginal]: "원래 상태",
|
|
13
|
+
[keys.debugCopy]: "JSON 복사"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ko };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=ko.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ko.js","names":[],"sources":["../../src/translations/ko.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const ko: Record<TranslationKey, string> = {\n\t[keys.undo]: '실행 취소',\n\t[keys.redo]: '다시 실행',\n\t[keys.debug]: '실행 취소 기록',\n\t[keys.debugTooltip]: '실행 취소 기록 살펴보기',\n\t[keys.debugTitle]: '실행 취소 기록',\n\t[keys.debugEmpty]: '아직 기록된 내역이 없습니다.',\n\t[keys.debugClose]: '닫기',\n\t[keys.debugPending]: '대기 중 (기록되지 않음)',\n\t[keys.debugOriginal]: '원래 상태',\n\t[keys.debugCopy]: 'JSON 복사',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/pt.ts
|
|
3
|
+
const pt = {
|
|
4
|
+
[keys.undo]: "Desfazer",
|
|
5
|
+
[keys.redo]: "Refazer",
|
|
6
|
+
[keys.debug]: "Histórico de desfazer",
|
|
7
|
+
[keys.debugTooltip]: "Inspecionar o histórico de desfazer",
|
|
8
|
+
[keys.debugTitle]: "Histórico de desfazer",
|
|
9
|
+
[keys.debugEmpty]: "Ainda não há histórico capturado.",
|
|
10
|
+
[keys.debugClose]: "Fechar",
|
|
11
|
+
[keys.debugPending]: "Pendente (não capturado)",
|
|
12
|
+
[keys.debugOriginal]: "Estado original",
|
|
13
|
+
[keys.debugCopy]: "Copiar JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { pt };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=pt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pt.js","names":[],"sources":["../../src/translations/pt.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const pt: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Desfazer',\n\t[keys.redo]: 'Refazer',\n\t[keys.debug]: 'Histórico de desfazer',\n\t[keys.debugTooltip]: 'Inspecionar o histórico de desfazer',\n\t[keys.debugTitle]: 'Histórico de desfazer',\n\t[keys.debugEmpty]: 'Ainda não há histórico capturado.',\n\t[keys.debugClose]: 'Fechar',\n\t[keys.debugPending]: 'Pendente (não capturado)',\n\t[keys.debugOriginal]: 'Estado original',\n\t[keys.debugCopy]: 'Copiar JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/ru.ts
|
|
3
|
+
const ru = {
|
|
4
|
+
[keys.undo]: "Отменить",
|
|
5
|
+
[keys.redo]: "Повторить",
|
|
6
|
+
[keys.debug]: "История отмен",
|
|
7
|
+
[keys.debugTooltip]: "Посмотреть историю отмен",
|
|
8
|
+
[keys.debugTitle]: "История отмен",
|
|
9
|
+
[keys.debugEmpty]: "История пока пуста.",
|
|
10
|
+
[keys.debugClose]: "Закрыть",
|
|
11
|
+
[keys.debugPending]: "Ожидает (не записано)",
|
|
12
|
+
[keys.debugOriginal]: "Исходное состояние",
|
|
13
|
+
[keys.debugCopy]: "Копировать JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { ru };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=ru.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ru.js","names":[],"sources":["../../src/translations/ru.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const ru: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Отменить',\n\t[keys.redo]: 'Повторить',\n\t[keys.debug]: 'История отмен',\n\t[keys.debugTooltip]: 'Посмотреть историю отмен',\n\t[keys.debugTitle]: 'История отмен',\n\t[keys.debugEmpty]: 'История пока пуста.',\n\t[keys.debugClose]: 'Закрыть',\n\t[keys.debugPending]: 'Ожидает (не записано)',\n\t[keys.debugOriginal]: 'Исходное состояние',\n\t[keys.debugCopy]: 'Копировать JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/uk.ts
|
|
3
|
+
const uk = {
|
|
4
|
+
[keys.undo]: "Скасувати",
|
|
5
|
+
[keys.redo]: "Повторити",
|
|
6
|
+
[keys.debug]: "Історія скасувань",
|
|
7
|
+
[keys.debugTooltip]: "Переглянути історію скасувань",
|
|
8
|
+
[keys.debugTitle]: "Історія скасувань",
|
|
9
|
+
[keys.debugEmpty]: "Історія поки порожня.",
|
|
10
|
+
[keys.debugClose]: "Закрити",
|
|
11
|
+
[keys.debugPending]: "Очікує (не записано)",
|
|
12
|
+
[keys.debugOriginal]: "Початковий стан",
|
|
13
|
+
[keys.debugCopy]: "Копіювати JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { uk };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=uk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uk.js","names":[],"sources":["../../src/translations/uk.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const uk: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Скасувати',\n\t[keys.redo]: 'Повторити',\n\t[keys.debug]: 'Історія скасувань',\n\t[keys.debugTooltip]: 'Переглянути історію скасувань',\n\t[keys.debugTitle]: 'Історія скасувань',\n\t[keys.debugEmpty]: 'Історія поки порожня.',\n\t[keys.debugClose]: 'Закрити',\n\t[keys.debugPending]: 'Очікує (не записано)',\n\t[keys.debugOriginal]: 'Початковий стан',\n\t[keys.debugCopy]: 'Копіювати JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { keys } from "./keys.js";
|
|
2
|
+
//#region src/translations/zh.ts
|
|
3
|
+
const zh = {
|
|
4
|
+
[keys.undo]: "撤销",
|
|
5
|
+
[keys.redo]: "重做",
|
|
6
|
+
[keys.debug]: "撤销历史",
|
|
7
|
+
[keys.debugTooltip]: "查看撤销历史",
|
|
8
|
+
[keys.debugTitle]: "撤销历史",
|
|
9
|
+
[keys.debugEmpty]: "尚未记录任何历史。",
|
|
10
|
+
[keys.debugClose]: "关闭",
|
|
11
|
+
[keys.debugPending]: "待处理(未记录)",
|
|
12
|
+
[keys.debugOriginal]: "原始状态",
|
|
13
|
+
[keys.debugCopy]: "复制 JSON"
|
|
14
|
+
};
|
|
15
|
+
//#endregion
|
|
16
|
+
export { zh };
|
|
17
|
+
|
|
18
|
+
//# sourceMappingURL=zh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zh.js","names":[],"sources":["../../src/translations/zh.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\nexport const zh: Record<TranslationKey, string> = {\n\t[keys.undo]: '撤销',\n\t[keys.redo]: '重做',\n\t[keys.debug]: '撤销历史',\n\t[keys.debugTooltip]: '查看撤销历史',\n\t[keys.debugTitle]: '撤销历史',\n\t[keys.debugEmpty]: '尚未记录任何历史。',\n\t[keys.debugClose]: '关闭',\n\t[keys.debugPending]: '待处理(未记录)',\n\t[keys.debugOriginal]: '原始状态',\n\t[keys.debugCopy]: '复制 JSON',\n}\n"],"mappings":";;AAEA,MAAa,KAAqC;EAChD,KAAK,OAAO;EACZ,KAAK,OAAO;EACZ,KAAK,QAAQ;EACb,KAAK,eAAe;EACpB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,aAAa;EAClB,KAAK,eAAe;EACpB,KAAK,gBAAgB;EACrB,KAAK,YAAY;AACnB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@10x-media/undo-redo",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Introduces undo/redo functionality to Payload forms",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -73,10 +73,10 @@
|
|
|
73
73
|
"tsdown": "0.22.1",
|
|
74
74
|
"typescript": "5.9.3",
|
|
75
75
|
"vitest": "4.1.7",
|
|
76
|
-
"@10x-media/payload-test-harness": "0.0.0",
|
|
77
76
|
"@10x-media/tsconfig": "0.0.0",
|
|
78
|
-
"@10x-media/
|
|
79
|
-
"@10x-media/
|
|
77
|
+
"@10x-media/tsdown-config": "0.0.0",
|
|
78
|
+
"@10x-media/payload-test-harness": "0.0.0",
|
|
79
|
+
"@10x-media/vitest-config": "0.0.0"
|
|
80
80
|
},
|
|
81
81
|
"publishConfig": {
|
|
82
82
|
"access": "public"
|