@10x-media/undo-redo 0.1.0-beta.0

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.
Files changed (60) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +54 -0
  4. package/dist/client/HistoryDebugOverlay.d.ts +30 -0
  5. package/dist/client/HistoryDebugOverlay.js +201 -0
  6. package/dist/client/HistoryDebugOverlay.js.map +1 -0
  7. package/dist/client/UndoRedoControls.d.ts +18 -0
  8. package/dist/client/UndoRedoControls.js +155 -0
  9. package/dist/client/UndoRedoControls.js.map +1 -0
  10. package/dist/client/formatShortcut.js +113 -0
  11. package/dist/client/formatShortcut.js.map +1 -0
  12. package/dist/client/historyDebugOverlay.css +206 -0
  13. package/dist/client/undoRedoControls.css +58 -0
  14. package/dist/client/useUndoRedo.d.ts +70 -0
  15. package/dist/client/useUndoRedo.js +259 -0
  16. package/dist/client/useUndoRedo.js.map +1 -0
  17. package/dist/exports/client.d.ts +4 -0
  18. package/dist/exports/client.js +5 -0
  19. package/dist/exports/i18n.d.ts +3 -0
  20. package/dist/exports/i18n.js +3 -0
  21. package/dist/exports/types.d.ts +5 -0
  22. package/dist/exports/types.js +1 -0
  23. package/dist/history/historyCore.d.ts +85 -0
  24. package/dist/history/historyCore.js +318 -0
  25. package/dist/history/historyCore.js.map +1 -0
  26. package/dist/history/pathPatterns.d.ts +6 -0
  27. package/dist/history/pathPatterns.js +45 -0
  28. package/dist/history/pathPatterns.js.map +1 -0
  29. package/dist/history/volatileValues.js +51 -0
  30. package/dist/history/volatileValues.js.map +1 -0
  31. package/dist/index.d.ts +20 -0
  32. package/dist/index.js +28 -0
  33. package/dist/index.js.map +1 -0
  34. package/dist/plugin/options.d.ts +109 -0
  35. package/dist/plugin/options.js +52 -0
  36. package/dist/plugin/options.js.map +1 -0
  37. package/dist/plugin/registerTranslations.js +19 -0
  38. package/dist/plugin/registerTranslations.js.map +1 -0
  39. package/dist/plugin/withUndoRedo.d.ts +34 -0
  40. package/dist/plugin/withUndoRedo.js +72 -0
  41. package/dist/plugin/withUndoRedo.js.map +1 -0
  42. package/dist/schema/fieldConfig.d.ts +36 -0
  43. package/dist/schema/fieldConfig.js +28 -0
  44. package/dist/schema/fieldConfig.js.map +1 -0
  45. package/dist/schema/fieldSchema.d.ts +63 -0
  46. package/dist/schema/fieldSchema.js +122 -0
  47. package/dist/schema/fieldSchema.js.map +1 -0
  48. package/dist/translations/de.js +19 -0
  49. package/dist/translations/de.js.map +1 -0
  50. package/dist/translations/en.js +23 -0
  51. package/dist/translations/en.js.map +1 -0
  52. package/dist/translations/index.d.ts +15 -0
  53. package/dist/translations/index.js +30 -0
  54. package/dist/translations/index.js.map +1 -0
  55. package/dist/translations/keys.d.ts +22 -0
  56. package/dist/translations/keys.js +22 -0
  57. package/dist/translations/keys.js.map +1 -0
  58. package/dist/translations/useTranslation.js +12 -0
  59. package/dist/translations/useTranslation.js.map +1 -0
  60. package/package.json +109 -0
@@ -0,0 +1,109 @@
1
+ import { TranslationsOption } from "../translations/index.js";
2
+ import { Field } from "payload";
3
+
4
+ //#region src/plugin/options.d.ts
5
+ /**
6
+ * Option shape and resolution. Kept free of Payload config plumbing so the
7
+ * precedence rules are testable on their own: they are the part a host is most
8
+ * likely to be surprised by.
9
+ */
10
+ /** One or more `react-hotkeys-hook` chords, e.g. `'mod+z'` or `['mod+y', 'mod+shift+z']`. */
11
+ type ShortcutKeys = string | string[];
12
+ /**
13
+ * Settings a single collection or global can override.
14
+ *
15
+ * Declared as a type alias rather than an interface so it satisfies
16
+ * `Record<string, unknown>`, which `definePlugin` constrains its options to.
17
+ */
18
+ type UndoRedoDocOptions = {
19
+ /** Mount the history inspector overlay behind an extra toolbar button. */debug?: boolean; /** Entries kept before the oldest is dropped. Defaults to 50. */
20
+ maxHistory?: number;
21
+ /**
22
+ * Milliseconds of quiet before edits are captured as one entry. Raise it to
23
+ * coalesce more typing into a single undo step, lower it for finer steps.
24
+ */
25
+ captureDebounce?: number;
26
+ /**
27
+ * Append the controls to the document controls automatically. Turn it off to
28
+ * place `<UndoRedoControls />` yourself in another admin slot.
29
+ */
30
+ autoMount?: boolean;
31
+ /**
32
+ * Form-state paths to leave out of the history, as patterns where `*` matches
33
+ * one path segment: `slug`, `list.*.readingTime`. A pattern also covers
34
+ * everything below it. Merged with, never replacing, the fields Payload owns.
35
+ *
36
+ * Use this for fields you cannot edit the definition of; for your own fields
37
+ * prefer `admin.custom` on the field itself, which survives renames.
38
+ */
39
+ ignorePaths?: string[]; /** Field types to leave out of the history wherever they appear. */
40
+ ignoreFieldTypes?: Field['type'][];
41
+ };
42
+ type UndoRedoPluginOptions = UndoRedoDocOptions & {
43
+ /** Disable the plugin entirely (incoming config returned untouched). */disabled?: boolean;
44
+ /**
45
+ * Keyboard chords, in `react-hotkeys-hook` syntax. `mod` resolves to Cmd on
46
+ * macOS and Ctrl elsewhere. Set to `false` to drop keyboard handling and keep
47
+ * the buttons.
48
+ *
49
+ * Deliberately global: shortcuts that change between collections are a way to
50
+ * confuse editors, not a feature.
51
+ */
52
+ shortcuts?: false | {
53
+ undo?: ShortcutKeys;
54
+ redo?: ShortcutKeys;
55
+ };
56
+ /**
57
+ * Per-collection settings. Undo/redo is on everywhere by default, so this is
58
+ * an opt-*out* map: `false` disables a collection, an object overrides the
59
+ * top-level settings for it. `collections: false` disables all of them.
60
+ */
61
+ collections?: false | Record<string, false | UndoRedoDocOptions>; /** Per-global settings, same shape as `collections`. */
62
+ globals?: false | Record<string, false | UndoRedoDocOptions>;
63
+ /**
64
+ * Per-locale overrides for this plugin's UI strings, keyed by the typed
65
+ * translation keys exported from `@10x-media/undo-redo/i18n`. Values win over
66
+ * the built-in locales key-by-key; locales the plugin does not ship are added
67
+ * whole. App-level `i18n.translations` still wins over both.
68
+ */
69
+ translations?: TranslationsOption;
70
+ };
71
+ declare const DEFAULT_CAPTURE_DEBOUNCE_MS = 400;
72
+ declare const DEFAULT_SHORTCUTS: {
73
+ readonly undo: readonly ["mod+z"];
74
+ readonly redo: readonly ["mod+shift+z", "mod+y"];
75
+ };
76
+ /** Fully resolved settings for one collection or global. */
77
+ interface ResolvedDocOptions {
78
+ autoMount: boolean;
79
+ debug: boolean;
80
+ maxHistory: number;
81
+ captureDebounce: number;
82
+ ignorePaths: string[];
83
+ ignoreFieldTypes: string[];
84
+ shortcuts: false | {
85
+ undo: string[];
86
+ redo: string[];
87
+ };
88
+ }
89
+ /** The subset the client component needs, and all of it JSON-serializable. */
90
+ type ControlsClientProps = Omit<ResolvedDocOptions, 'autoMount'>;
91
+ type DocScope = 'collections' | 'globals';
92
+ /**
93
+ * Resolve the settings for one document type, or `null` when undo/redo is off
94
+ * for it.
95
+ *
96
+ * Precedence is plugin defaults, then top-level options, then the per-slug
97
+ * entry. `ignorePaths` and `ignoreFieldTypes` merge across those layers instead
98
+ * of replacing: a collection adding one path should not silently drop the ones
99
+ * declared globally, nor the fields Payload owns.
100
+ */
101
+ declare const resolveDocOptions: (options: UndoRedoPluginOptions, scope: DocScope, slug: string) => ResolvedDocOptions | null;
102
+ /** Strip the settings only the config layer uses, leaving the client's props. */
103
+ declare const toClientProps: ({
104
+ autoMount: _autoMount,
105
+ ...clientProps
106
+ }: ResolvedDocOptions) => ControlsClientProps;
107
+ //#endregion
108
+ export { ControlsClientProps, DEFAULT_CAPTURE_DEBOUNCE_MS, DEFAULT_SHORTCUTS, DocScope, ResolvedDocOptions, ShortcutKeys, UndoRedoDocOptions, UndoRedoPluginOptions, resolveDocOptions, toClientProps };
109
+ //# sourceMappingURL=options.d.ts.map
@@ -0,0 +1,52 @@
1
+ import { DEFAULT_IGNORED_PATHS } from "../history/historyCore.js";
2
+ //#region src/plugin/options.ts
3
+ const DEFAULT_CAPTURE_DEBOUNCE_MS = 400;
4
+ const DEFAULT_SHORTCUTS = {
5
+ undo: ["mod+z"],
6
+ redo: ["mod+shift+z", "mod+y"]
7
+ };
8
+ const toArray = (keys, fallback) => keys === void 0 ? [...fallback] : Array.isArray(keys) ? [...keys] : [keys];
9
+ const resolveShortcuts = (options) => {
10
+ if (options.shortcuts === false) return false;
11
+ return {
12
+ undo: toArray(options.shortcuts?.undo, DEFAULT_SHORTCUTS.undo),
13
+ redo: toArray(options.shortcuts?.redo, DEFAULT_SHORTCUTS.redo)
14
+ };
15
+ };
16
+ /**
17
+ * Resolve the settings for one document type, or `null` when undo/redo is off
18
+ * for it.
19
+ *
20
+ * Precedence is plugin defaults, then top-level options, then the per-slug
21
+ * entry. `ignorePaths` and `ignoreFieldTypes` merge across those layers instead
22
+ * of replacing: a collection adding one path should not silently drop the ones
23
+ * declared globally, nor the fields Payload owns.
24
+ */
25
+ const resolveDocOptions = (options, scope, slug) => {
26
+ const scoped = options[scope];
27
+ if (scoped === false) return null;
28
+ const entry = scoped?.[slug];
29
+ if (entry === false) return null;
30
+ const layers = entry ? [options, entry] : [options];
31
+ const pick = (key) => {
32
+ for (let i = layers.length - 1; i >= 0; i--) {
33
+ const value = layers[i]?.[key];
34
+ if (value !== void 0) return value;
35
+ }
36
+ };
37
+ return {
38
+ autoMount: pick("autoMount") ?? true,
39
+ debug: pick("debug") === true,
40
+ maxHistory: pick("maxHistory") ?? 50,
41
+ captureDebounce: pick("captureDebounce") ?? 400,
42
+ ignorePaths: [...new Set([...DEFAULT_IGNORED_PATHS, ...layers.flatMap((layer) => layer.ignorePaths ?? [])])],
43
+ ignoreFieldTypes: [...new Set(layers.flatMap((layer) => layer.ignoreFieldTypes ?? []))],
44
+ shortcuts: resolveShortcuts(options)
45
+ };
46
+ };
47
+ /** Strip the settings only the config layer uses, leaving the client's props. */
48
+ const toClientProps = ({ autoMount: _autoMount, ...clientProps }) => clientProps;
49
+ //#endregion
50
+ export { DEFAULT_CAPTURE_DEBOUNCE_MS, DEFAULT_SHORTCUTS, resolveDocOptions, toClientProps };
51
+
52
+ //# sourceMappingURL=options.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"options.js","names":[],"sources":["../../src/plugin/options.ts"],"sourcesContent":["import type { Field } from 'payload'\n\nimport { DEFAULT_IGNORED_PATHS, MAX_HISTORY_ENTRIES } from '../history/historyCore'\nimport type { TranslationsOption } from '../translations'\n\n/**\n * Option shape and resolution. Kept free of Payload config plumbing so the\n * precedence rules are testable on their own: they are the part a host is most\n * likely to be surprised by.\n */\n\n/** One or more `react-hotkeys-hook` chords, e.g. `'mod+z'` or `['mod+y', 'mod+shift+z']`. */\nexport type ShortcutKeys = string | string[]\n\n/**\n * Settings a single collection or global can override.\n *\n * Declared as a type alias rather than an interface so it satisfies\n * `Record<string, unknown>`, which `definePlugin` constrains its options to.\n */\nexport type UndoRedoDocOptions = {\n\t/** Mount the history inspector overlay behind an extra toolbar button. */\n\tdebug?: boolean\n\t/** Entries kept before the oldest is dropped. Defaults to 50. */\n\tmaxHistory?: number\n\t/**\n\t * Milliseconds of quiet before edits are captured as one entry. Raise it to\n\t * coalesce more typing into a single undo step, lower it for finer steps.\n\t */\n\tcaptureDebounce?: number\n\t/**\n\t * Append the controls to the document controls automatically. Turn it off to\n\t * place `<UndoRedoControls />` yourself in another admin slot.\n\t */\n\tautoMount?: boolean\n\t/**\n\t * Form-state paths to leave out of the history, as patterns where `*` matches\n\t * one path segment: `slug`, `list.*.readingTime`. A pattern also covers\n\t * everything below it. Merged with, never replacing, the fields Payload owns.\n\t *\n\t * Use this for fields you cannot edit the definition of; for your own fields\n\t * prefer `admin.custom` on the field itself, which survives renames.\n\t */\n\tignorePaths?: string[]\n\t/** Field types to leave out of the history wherever they appear. */\n\tignoreFieldTypes?: Field['type'][]\n}\n\nexport type UndoRedoPluginOptions = UndoRedoDocOptions & {\n\t/** Disable the plugin entirely (incoming config returned untouched). */\n\tdisabled?: boolean\n\t/**\n\t * Keyboard chords, in `react-hotkeys-hook` syntax. `mod` resolves to Cmd on\n\t * macOS and Ctrl elsewhere. Set to `false` to drop keyboard handling and keep\n\t * the buttons.\n\t *\n\t * Deliberately global: shortcuts that change between collections are a way to\n\t * confuse editors, not a feature.\n\t */\n\tshortcuts?: false | { undo?: ShortcutKeys; redo?: ShortcutKeys }\n\t/**\n\t * Per-collection settings. Undo/redo is on everywhere by default, so this is\n\t * an opt-*out* map: `false` disables a collection, an object overrides the\n\t * top-level settings for it. `collections: false` disables all of them.\n\t */\n\tcollections?: false | Record<string, false | UndoRedoDocOptions>\n\t/** Per-global settings, same shape as `collections`. */\n\tglobals?: false | Record<string, false | UndoRedoDocOptions>\n\t/**\n\t * Per-locale overrides for this plugin's UI strings, keyed by the typed\n\t * translation keys exported from `@10x-media/undo-redo/i18n`. Values win over\n\t * the built-in locales key-by-key; locales the plugin does not ship are added\n\t * whole. App-level `i18n.translations` still wins over both.\n\t */\n\ttranslations?: TranslationsOption\n}\n\nexport const DEFAULT_CAPTURE_DEBOUNCE_MS = 400\n\nexport const DEFAULT_SHORTCUTS = {\n\tundo: ['mod+z'],\n\tredo: ['mod+shift+z', 'mod+y'],\n} as const\n\n/** Fully resolved settings for one collection or global. */\nexport interface ResolvedDocOptions {\n\tautoMount: boolean\n\tdebug: boolean\n\tmaxHistory: number\n\tcaptureDebounce: number\n\tignorePaths: string[]\n\tignoreFieldTypes: string[]\n\tshortcuts: false | { undo: string[]; redo: string[] }\n}\n\n/** The subset the client component needs, and all of it JSON-serializable. */\nexport type ControlsClientProps = Omit<ResolvedDocOptions, 'autoMount'>\n\nconst toArray = (keys: ShortcutKeys | undefined, fallback: readonly string[]): string[] =>\n\tkeys === undefined ? [...fallback] : Array.isArray(keys) ? [...keys] : [keys]\n\nconst resolveShortcuts = (options: UndoRedoPluginOptions): ResolvedDocOptions['shortcuts'] => {\n\tif (options.shortcuts === false) return false\n\treturn {\n\t\tundo: toArray(options.shortcuts?.undo, DEFAULT_SHORTCUTS.undo),\n\t\tredo: toArray(options.shortcuts?.redo, DEFAULT_SHORTCUTS.redo),\n\t}\n}\n\nexport type DocScope = 'collections' | 'globals'\n\n/**\n * Resolve the settings for one document type, or `null` when undo/redo is off\n * for it.\n *\n * Precedence is plugin defaults, then top-level options, then the per-slug\n * entry. `ignorePaths` and `ignoreFieldTypes` merge across those layers instead\n * of replacing: a collection adding one path should not silently drop the ones\n * declared globally, nor the fields Payload owns.\n */\nexport const resolveDocOptions = (\n\toptions: UndoRedoPluginOptions,\n\tscope: DocScope,\n\tslug: string\n): ResolvedDocOptions | null => {\n\tconst scoped = options[scope]\n\tif (scoped === false) return null\n\tconst entry = scoped?.[slug]\n\tif (entry === false) return null\n\n\tconst layers: UndoRedoDocOptions[] = entry ? [options, entry] : [options]\n\tconst pick = <K extends keyof UndoRedoDocOptions>(key: K): UndoRedoDocOptions[K] | undefined => {\n\t\tfor (let i = layers.length - 1; i >= 0; i--) {\n\t\t\tconst value = layers[i]?.[key]\n\t\t\tif (value !== undefined) return value\n\t\t}\n\t\treturn undefined\n\t}\n\n\treturn {\n\t\tautoMount: pick('autoMount') ?? true,\n\t\tdebug: pick('debug') === true,\n\t\tmaxHistory: pick('maxHistory') ?? MAX_HISTORY_ENTRIES,\n\t\tcaptureDebounce: pick('captureDebounce') ?? DEFAULT_CAPTURE_DEBOUNCE_MS,\n\t\tignorePaths: [\n\t\t\t...new Set([...DEFAULT_IGNORED_PATHS, ...layers.flatMap((layer) => layer.ignorePaths ?? [])]),\n\t\t],\n\t\tignoreFieldTypes: [...new Set(layers.flatMap((layer) => layer.ignoreFieldTypes ?? []))],\n\t\tshortcuts: resolveShortcuts(options),\n\t}\n}\n\n/** Strip the settings only the config layer uses, leaving the client's props. */\nexport const toClientProps = ({\n\tautoMount: _autoMount,\n\t...clientProps\n}: ResolvedDocOptions): ControlsClientProps => clientProps\n"],"mappings":";;AA6EA,MAAa,8BAA8B;AAE3C,MAAa,oBAAoB;CAChC,MAAM,CAAC,OAAO;CACd,MAAM,CAAC,eAAe,OAAO;AAC9B;AAgBA,MAAM,WAAW,MAAgC,aAChD,SAAS,KAAA,IAAY,CAAC,GAAG,QAAQ,IAAI,MAAM,QAAQ,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,IAAI;AAE7E,MAAM,oBAAoB,YAAoE;CAC7F,IAAI,QAAQ,cAAc,OAAO,OAAO;CACxC,OAAO;EACN,MAAM,QAAQ,QAAQ,WAAW,MAAM,kBAAkB,IAAI;EAC7D,MAAM,QAAQ,QAAQ,WAAW,MAAM,kBAAkB,IAAI;CAC9D;AACD;;;;;;;;;;AAaA,MAAa,qBACZ,SACA,OACA,SAC+B;CAC/B,MAAM,SAAS,QAAQ;CACvB,IAAI,WAAW,OAAO,OAAO;CAC7B,MAAM,QAAQ,SAAS;CACvB,IAAI,UAAU,OAAO,OAAO;CAE5B,MAAM,SAA+B,QAAQ,CAAC,SAAS,KAAK,IAAI,CAAC,OAAO;CACxE,MAAM,QAA4C,QAA8C;EAC/F,KAAK,IAAI,IAAI,OAAO,SAAS,GAAG,KAAK,GAAG,KAAK;GAC5C,MAAM,QAAQ,OAAO,KAAK;GAC1B,IAAI,UAAU,KAAA,GAAW,OAAO;EACjC;CAED;CAEA,OAAO;EACN,WAAW,KAAK,WAAW,KAAK;EAChC,OAAO,KAAK,OAAO,MAAM;EACzB,YAAY,KAAK,YAAY,KAAA;EAC7B,iBAAiB,KAAK,iBAAiB,KAAA;EACvC,aAAa,CACZ,GAAG,IAAI,IAAI,CAAC,GAAG,uBAAuB,GAAG,OAAO,SAAS,UAAU,MAAM,eAAe,CAAC,CAAC,CAAC,CAAC,CAC7F;EACA,kBAAkB,CAAC,GAAG,IAAI,IAAI,OAAO,SAAS,UAAU,MAAM,oBAAoB,CAAC,CAAC,CAAC,CAAC;EACtF,WAAW,iBAAiB,OAAO;CACpC;AACD;;AAGA,MAAa,iBAAiB,EAC7B,WAAW,YACX,GAAG,kBAC2C"}
@@ -0,0 +1,19 @@
1
+ import { toNested, translations } from "../translations/index.js";
2
+ import { deepMergeSimple } from "payload/shared";
3
+ //#region src/plugin/registerTranslations.ts
4
+ /**
5
+ * Merge this plugin's translations into the host config. Plugin-level
6
+ * `overrides` win over the built-ins key-by-key and may add locales the plugin
7
+ * does not ship. A host value wins over both (`deepMergeSimple` lets the second
8
+ * argument override), so projects can override any string.
9
+ */
10
+ const registerTranslations = (config, overrides) => {
11
+ const nested = {};
12
+ for (const [locale, flat] of Object.entries(overrides ?? {})) nested[locale] = toNested(flat);
13
+ config.i18n ??= {};
14
+ config.i18n.translations = deepMergeSimple(deepMergeSimple(translations, nested), config.i18n.translations ?? {});
15
+ };
16
+ //#endregion
17
+ export { registerTranslations };
18
+
19
+ //# sourceMappingURL=registerTranslations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registerTranslations.js","names":[],"sources":["../../src/plugin/registerTranslations.ts"],"sourcesContent":["import type { Config } from 'payload'\nimport { deepMergeSimple } from 'payload/shared'\n\nimport { type TranslationsOption, toNested, translations } from '../translations'\n\ntype Translations = NonNullable<NonNullable<Config['i18n']>['translations']>\n\n/**\n * Merge this plugin's translations into the host config. Plugin-level\n * `overrides` win over the built-ins key-by-key and may add locales the plugin\n * does not ship. A host value wins over both (`deepMergeSimple` lets the second\n * argument override), so projects can override any string.\n */\nexport const registerTranslations = (config: Config, overrides?: TranslationsOption): void => {\n\tconst nested: Record<string, Record<string, Record<string, string>>> = {}\n\tfor (const [locale, flat] of Object.entries(overrides ?? {})) {\n\t\tnested[locale] = toNested(flat)\n\t}\n\tconfig.i18n ??= {}\n\tconfig.i18n.translations = deepMergeSimple<Translations>(\n\t\tdeepMergeSimple(translations, nested),\n\t\tconfig.i18n.translations ?? {}\n\t)\n}\n"],"mappings":";;;;;;;;;AAaA,MAAa,wBAAwB,QAAgB,cAAyC;CAC7F,MAAM,SAAiE,CAAC;CACxE,KAAK,MAAM,CAAC,QAAQ,SAAS,OAAO,QAAQ,aAAa,CAAC,CAAC,GAC1D,OAAO,UAAU,SAAS,IAAI;CAE/B,OAAO,SAAS,CAAC;CACjB,OAAO,KAAK,eAAe,gBAC1B,gBAAgB,cAAc,MAAM,GACpC,OAAO,KAAK,gBAAgB,CAAC,CAC9B;AACD"}
@@ -0,0 +1,34 @@
1
+ import { ResolvedDocOptions } from "./options.js";
2
+ import { CollectionConfig, GlobalConfig, PayloadComponent } from "payload";
3
+
4
+ //#region src/plugin/withUndoRedo.d.ts
5
+ /** Client component path resolved through the package export map by the import map. */
6
+ declare const UNDO_REDO_COMPONENT_PATH = "@10x-media/undo-redo/client#UndoRedoControls";
7
+ /**
8
+ * The component entry to append to a `beforeDocumentControls` slot, carrying
9
+ * the resolved settings as client props.
10
+ *
11
+ * The settings travel as props rather than being re-derived on the client so
12
+ * there is a single resolution path: whatever the config says is what the
13
+ * mounted controls use, and the debug overlay shows exactly that.
14
+ */
15
+ declare const undoRedoComponent: (options: ResolvedDocOptions) => PayloadComponent<never, never>;
16
+ /**
17
+ * Add the undo/redo controls to a collection's edit view by appending to
18
+ * `admin.components.edit.beforeDocumentControls`, preserving any components
19
+ * the config already declares.
20
+ *
21
+ * A `null` options object means undo/redo is off for this collection, and
22
+ * `autoMount: false` means the host mounts the controls itself; both return the
23
+ * config untouched.
24
+ */
25
+ declare const withUndoRedo: (config: CollectionConfig, options: ResolvedDocOptions | null) => CollectionConfig;
26
+ /**
27
+ * Global variant: globals declare the same slot under
28
+ * `admin.components.elements.beforeDocumentControls` (see
29
+ * @payloadcms/next renderDocumentSlots).
30
+ */
31
+ declare const withUndoRedoGlobal: (config: GlobalConfig, options: ResolvedDocOptions | null) => GlobalConfig;
32
+ //#endregion
33
+ export { UNDO_REDO_COMPONENT_PATH, undoRedoComponent, withUndoRedo, withUndoRedoGlobal };
34
+ //# sourceMappingURL=withUndoRedo.d.ts.map
@@ -0,0 +1,72 @@
1
+ import { toClientProps } from "./options.js";
2
+ //#region src/plugin/withUndoRedo.ts
3
+ /** Client component path resolved through the package export map by the import map. */
4
+ const UNDO_REDO_COMPONENT_PATH = "@10x-media/undo-redo/client#UndoRedoControls";
5
+ /**
6
+ * The component entry to append to a `beforeDocumentControls` slot, carrying
7
+ * the resolved settings as client props.
8
+ *
9
+ * The settings travel as props rather than being re-derived on the client so
10
+ * there is a single resolution path: whatever the config says is what the
11
+ * mounted controls use, and the debug overlay shows exactly that.
12
+ */
13
+ const undoRedoComponent = (options) => ({
14
+ path: UNDO_REDO_COMPONENT_PATH,
15
+ clientProps: toClientProps(options)
16
+ });
17
+ /**
18
+ * Add the undo/redo controls to a collection's edit view by appending to
19
+ * `admin.components.edit.beforeDocumentControls`, preserving any components
20
+ * the config already declares.
21
+ *
22
+ * A `null` options object means undo/redo is off for this collection, and
23
+ * `autoMount: false` means the host mounts the controls itself; both return the
24
+ * config untouched.
25
+ */
26
+ const withUndoRedo = (config, options) => {
27
+ if (!options?.autoMount) return config;
28
+ const admin = config.admin ?? {};
29
+ const components = admin.components ?? {};
30
+ const edit = components.edit ?? {};
31
+ return {
32
+ ...config,
33
+ admin: {
34
+ ...admin,
35
+ components: {
36
+ ...components,
37
+ edit: {
38
+ ...edit,
39
+ beforeDocumentControls: [...edit.beforeDocumentControls ?? [], undoRedoComponent(options)]
40
+ }
41
+ }
42
+ }
43
+ };
44
+ };
45
+ /**
46
+ * Global variant: globals declare the same slot under
47
+ * `admin.components.elements.beforeDocumentControls` (see
48
+ * @payloadcms/next renderDocumentSlots).
49
+ */
50
+ const withUndoRedoGlobal = (config, options) => {
51
+ if (!options?.autoMount) return config;
52
+ const admin = config.admin ?? {};
53
+ const components = admin.components ?? {};
54
+ const elements = components.elements ?? {};
55
+ return {
56
+ ...config,
57
+ admin: {
58
+ ...admin,
59
+ components: {
60
+ ...components,
61
+ elements: {
62
+ ...elements,
63
+ beforeDocumentControls: [...elements.beforeDocumentControls ?? [], undoRedoComponent(options)]
64
+ }
65
+ }
66
+ }
67
+ };
68
+ };
69
+ //#endregion
70
+ export { UNDO_REDO_COMPONENT_PATH, undoRedoComponent, withUndoRedo, withUndoRedoGlobal };
71
+
72
+ //# sourceMappingURL=withUndoRedo.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"withUndoRedo.js","names":[],"sources":["../../src/plugin/withUndoRedo.ts"],"sourcesContent":["import type { CollectionConfig, GlobalConfig, PayloadComponent } from 'payload'\n\nimport { type ResolvedDocOptions, toClientProps } from './options'\n\n/** Client component path resolved through the package export map by the import map. */\nexport const UNDO_REDO_COMPONENT_PATH = '@10x-media/undo-redo/client#UndoRedoControls'\n\n/**\n * The component entry to append to a `beforeDocumentControls` slot, carrying\n * the resolved settings as client props.\n *\n * The settings travel as props rather than being re-derived on the client so\n * there is a single resolution path: whatever the config says is what the\n * mounted controls use, and the debug overlay shows exactly that.\n */\nexport const undoRedoComponent = (options: ResolvedDocOptions): PayloadComponent<never, never> => ({\n\tpath: UNDO_REDO_COMPONENT_PATH,\n\tclientProps: toClientProps(options),\n})\n\n/**\n * Add the undo/redo controls to a collection's edit view by appending to\n * `admin.components.edit.beforeDocumentControls`, preserving any components\n * the config already declares.\n *\n * A `null` options object means undo/redo is off for this collection, and\n * `autoMount: false` means the host mounts the controls itself; both return the\n * config untouched.\n */\nexport const withUndoRedo = (\n\tconfig: CollectionConfig,\n\toptions: ResolvedDocOptions | null\n): CollectionConfig => {\n\tif (!options?.autoMount) return config\n\tconst admin = config.admin ?? {}\n\tconst components = admin.components ?? {}\n\tconst edit = components.edit ?? {}\n\treturn {\n\t\t...config,\n\t\tadmin: {\n\t\t\t...admin,\n\t\t\tcomponents: {\n\t\t\t\t...components,\n\t\t\t\tedit: {\n\t\t\t\t\t...edit,\n\t\t\t\t\tbeforeDocumentControls: [\n\t\t\t\t\t\t...(edit.beforeDocumentControls ?? []),\n\t\t\t\t\t\tundoRedoComponent(options),\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n\n/**\n * Global variant: globals declare the same slot under\n * `admin.components.elements.beforeDocumentControls` (see\n * @payloadcms/next renderDocumentSlots).\n */\nexport const withUndoRedoGlobal = (\n\tconfig: GlobalConfig,\n\toptions: ResolvedDocOptions | null\n): GlobalConfig => {\n\tif (!options?.autoMount) return config\n\tconst admin = config.admin ?? {}\n\tconst components = admin.components ?? {}\n\tconst elements = components.elements ?? {}\n\treturn {\n\t\t...config,\n\t\tadmin: {\n\t\t\t...admin,\n\t\t\tcomponents: {\n\t\t\t\t...components,\n\t\t\t\telements: {\n\t\t\t\t\t...elements,\n\t\t\t\t\tbeforeDocumentControls: [\n\t\t\t\t\t\t...(elements.beforeDocumentControls ?? []),\n\t\t\t\t\t\tundoRedoComponent(options),\n\t\t\t\t\t],\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n}\n"],"mappings":";;;AAKA,MAAa,2BAA2B;;;;;;;;;AAUxC,MAAa,qBAAqB,aAAiE;CAClG,MAAM;CACN,aAAa,cAAc,OAAO;AACnC;;;;;;;;;;AAWA,MAAa,gBACZ,QACA,YACsB;CACtB,IAAI,CAAC,SAAS,WAAW,OAAO;CAChC,MAAM,QAAQ,OAAO,SAAS,CAAC;CAC/B,MAAM,aAAa,MAAM,cAAc,CAAC;CACxC,MAAM,OAAO,WAAW,QAAQ,CAAC;CACjC,OAAO;EACN,GAAG;EACH,OAAO;GACN,GAAG;GACH,YAAY;IACX,GAAG;IACH,MAAM;KACL,GAAG;KACH,wBAAwB,CACvB,GAAI,KAAK,0BAA0B,CAAC,GACpC,kBAAkB,OAAO,CAC1B;IACD;GACD;EACD;CACD;AACD;;;;;;AAOA,MAAa,sBACZ,QACA,YACkB;CAClB,IAAI,CAAC,SAAS,WAAW,OAAO;CAChC,MAAM,QAAQ,OAAO,SAAS,CAAC;CAC/B,MAAM,aAAa,MAAM,cAAc,CAAC;CACxC,MAAM,WAAW,WAAW,YAAY,CAAC;CACzC,OAAO;EACN,GAAG;EACH,OAAO;GACN,GAAG;GACH,YAAY;IACX,GAAG;IACH,UAAU;KACT,GAAG;KACH,wBAAwB,CACvB,GAAI,SAAS,0BAA0B,CAAC,GACxC,kBAAkB,OAAO,CAC1B;IACD;GACD;EACD;CACD;AACD"}
@@ -0,0 +1,36 @@
1
+ //#region src/schema/fieldConfig.d.ts
2
+ /**
3
+ * Per-field opt-out, declared next to the field it applies to.
4
+ *
5
+ * It has to live under `admin.custom` rather than the field's own `custom`:
6
+ * Payload strips `custom` from the client config (it is listed in
7
+ * `serverOnlyFieldProperties`, see payload/src/fields/config/client.ts), while
8
+ * `admin.custom` is documented as available on both sides and survives. The
9
+ * controls run on the client, so `admin.custom` is the only channel that
10
+ * reaches them.
11
+ */
12
+ /** Namespace this plugin claims inside the shared `admin.custom` object. */
13
+ declare const UNDO_REDO_FIELD_KEY = "undoRedo";
14
+ interface UndoRedoFieldConfig {
15
+ /**
16
+ * Leave this field out of the undo history entirely: its changes create no
17
+ * entries and undo never writes to it. Applies to the whole subtree when set
18
+ * on anything with children: a group, array, blocks field, tabs field, an
19
+ * individual tab, a row or a collapsible.
20
+ */
21
+ disabled?: boolean;
22
+ }
23
+ /**
24
+ * Build the `admin.custom` fragment for a field.
25
+ *
26
+ * ```ts
27
+ * { name: 'content', type: 'richText', admin: { custom: undoRedoCustom({ disabled: true }) } }
28
+ * ```
29
+ *
30
+ * Spread it when the field already carries other custom data:
31
+ * `custom: { ...existing, ...undoRedoCustom({ disabled: true }) }`.
32
+ */
33
+ declare const undoRedoCustom: (config: UndoRedoFieldConfig) => Record<string, UndoRedoFieldConfig>;
34
+ //#endregion
35
+ export { UNDO_REDO_FIELD_KEY, UndoRedoFieldConfig, undoRedoCustom };
36
+ //# sourceMappingURL=fieldConfig.d.ts.map
@@ -0,0 +1,28 @@
1
+ //#region src/schema/fieldConfig.ts
2
+ /**
3
+ * Per-field opt-out, declared next to the field it applies to.
4
+ *
5
+ * It has to live under `admin.custom` rather than the field's own `custom`:
6
+ * Payload strips `custom` from the client config (it is listed in
7
+ * `serverOnlyFieldProperties`, see payload/src/fields/config/client.ts), while
8
+ * `admin.custom` is documented as available on both sides and survives. The
9
+ * controls run on the client, so `admin.custom` is the only channel that
10
+ * reaches them.
11
+ */
12
+ /** Namespace this plugin claims inside the shared `admin.custom` object. */
13
+ const UNDO_REDO_FIELD_KEY = "undoRedo";
14
+ /**
15
+ * Build the `admin.custom` fragment for a field.
16
+ *
17
+ * ```ts
18
+ * { name: 'content', type: 'richText', admin: { custom: undoRedoCustom({ disabled: true }) } }
19
+ * ```
20
+ *
21
+ * Spread it when the field already carries other custom data:
22
+ * `custom: { ...existing, ...undoRedoCustom({ disabled: true }) }`.
23
+ */
24
+ const undoRedoCustom = (config) => ({ [UNDO_REDO_FIELD_KEY]: config });
25
+ //#endregion
26
+ export { UNDO_REDO_FIELD_KEY, undoRedoCustom };
27
+
28
+ //# sourceMappingURL=fieldConfig.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldConfig.js","names":[],"sources":["../../src/schema/fieldConfig.ts"],"sourcesContent":["/**\n * Per-field opt-out, declared next to the field it applies to.\n *\n * It has to live under `admin.custom` rather than the field's own `custom`:\n * Payload strips `custom` from the client config (it is listed in\n * `serverOnlyFieldProperties`, see payload/src/fields/config/client.ts), while\n * `admin.custom` is documented as available on both sides and survives. The\n * controls run on the client, so `admin.custom` is the only channel that\n * reaches them.\n */\n\n/** Namespace this plugin claims inside the shared `admin.custom` object. */\nexport const UNDO_REDO_FIELD_KEY = 'undoRedo'\n\nexport interface UndoRedoFieldConfig {\n\t/**\n\t * Leave this field out of the undo history entirely: its changes create no\n\t * entries and undo never writes to it. Applies to the whole subtree when set\n\t * on anything with children: a group, array, blocks field, tabs field, an\n\t * individual tab, a row or a collapsible.\n\t */\n\tdisabled?: boolean\n}\n\n/**\n * Build the `admin.custom` fragment for a field.\n *\n * ```ts\n * { name: 'content', type: 'richText', admin: { custom: undoRedoCustom({ disabled: true }) } }\n * ```\n *\n * Spread it when the field already carries other custom data:\n * `custom: { ...existing, ...undoRedoCustom({ disabled: true }) }`.\n */\nexport const undoRedoCustom = (\n\tconfig: UndoRedoFieldConfig\n): Record<string, UndoRedoFieldConfig> => ({ [UNDO_REDO_FIELD_KEY]: config })\n"],"mappings":";;;;;;;;;;;;AAYA,MAAa,sBAAsB;;;;;;;;;;;AAsBnC,MAAa,kBACZ,YAC0C,GAAG,sBAAsB,OAAO"}
@@ -0,0 +1,63 @@
1
+ //#region src/schema/fieldSchema.d.ts
2
+ /**
3
+ * Walks a Payload field schema and flattens it into the path patterns the
4
+ * history matcher speaks, annotating each with the field's type and whether the
5
+ * host opted it out of undo/redo.
6
+ *
7
+ * This exists because form state carries no type information: `FormField` has a
8
+ * `fieldSchema` member, but only when `buildFormState` is called with
9
+ * `includeSchema: true`, which the document edit view never does. The client
10
+ * config is the only place a mounted component can learn that `layout.0.body`
11
+ * is rich text rather than a plain string.
12
+ *
13
+ * Row indexes collapse to `*`, because a schema describes every row at once:
14
+ * an array `list` containing `title` yields `list.*.title`, which the matcher
15
+ * then tests against the concrete `list.0.title`.
16
+ */
17
+ /** Anything that can carry this plugin's per-field opt-out. */
18
+ interface CustomCarrier {
19
+ admin?: {
20
+ custom?: Record<string, unknown>;
21
+ };
22
+ }
23
+ /** The structural shape this walk needs, satisfied by both `Field` and `ClientField`. */
24
+ interface WalkableField extends CustomCarrier {
25
+ type: string;
26
+ name?: string;
27
+ fields?: WalkableField[];
28
+ blocks?: (WalkableBlock | string)[];
29
+ blockReferences?: (WalkableBlock | string)[];
30
+ tabs?: WalkableTab[];
31
+ }
32
+ interface WalkableBlock {
33
+ slug: string;
34
+ fields: WalkableField[];
35
+ }
36
+ interface WalkableTab extends CustomCarrier {
37
+ name?: string;
38
+ fields: WalkableField[];
39
+ }
40
+ interface FieldSchemaEntry {
41
+ /** Form-state path pattern, with `*` standing in for every array or blocks row index. */
42
+ pattern: string;
43
+ /**
44
+ * The field's type, or `undefined` when the pattern is ambiguous: two blocks
45
+ * in one blocks field can declare the same field name with different types,
46
+ * and a form-state path carries no block discriminator to tell them apart.
47
+ * Callers must treat `undefined` as "unknown" and fall back to inspecting the
48
+ * value, never as "no field here".
49
+ */
50
+ type: string | undefined;
51
+ /** Every type seen at this pattern, so ambiguity is inspectable rather than silent. */
52
+ types: string[];
53
+ /**
54
+ * Whether any contributor marked the field opted out. Ambiguous patterns
55
+ * resolve by OR: honouring an opt-out that was meant for a sibling block is
56
+ * the safe direction, ignoring one the host asked for is not.
57
+ */
58
+ disabled: boolean;
59
+ }
60
+ type FieldSchemaMap = Map<string, FieldSchemaEntry>;
61
+ //#endregion
62
+ export { FieldSchemaEntry, FieldSchemaMap, WalkableBlock, WalkableField, WalkableTab };
63
+ //# sourceMappingURL=fieldSchema.d.ts.map
@@ -0,0 +1,122 @@
1
+ import { UNDO_REDO_FIELD_KEY } from "./fieldConfig.js";
2
+ //#region src/schema/fieldSchema.ts
3
+ const readFieldConfig = (field) => field.admin?.custom?.[UNDO_REDO_FIELD_KEY];
4
+ const isDisabled = (field) => readFieldConfig(field)?.disabled === true;
5
+ const record = (context, pattern, found) => {
6
+ const existing = context.map.get(pattern);
7
+ if (!existing) {
8
+ context.map.set(pattern, {
9
+ pattern,
10
+ type: found.type,
11
+ types: [found.type],
12
+ disabled: found.disabled
13
+ });
14
+ return;
15
+ }
16
+ if (!existing.types.includes(found.type)) {
17
+ existing.types.push(found.type);
18
+ existing.type = void 0;
19
+ }
20
+ existing.disabled ||= found.disabled;
21
+ };
22
+ const join = (parent, segment) => parent === "" ? segment : `${parent}.${segment}`;
23
+ /** `blockReferences` wins when present: Payload populates one or the other. */
24
+ const resolveBlocks = (field) => field.blockReferences ?? field.blocks ?? [];
25
+ const walkBlock = (block, scope, context) => {
26
+ const resolved = typeof block === "string" ? context.blocksMap?.[block] : block;
27
+ if (!resolved || context.openBlocks.has(resolved.slug)) return;
28
+ context.openBlocks.add(resolved.slug);
29
+ walkFields(resolved.fields, scope, context);
30
+ context.openBlocks.delete(resolved.slug);
31
+ };
32
+ const walkFields = (fields, scope, context) => {
33
+ for (const field of fields) {
34
+ if (field.type === "ui") continue;
35
+ if (field.type === "tabs") {
36
+ const disabled = scope.disabled || isDisabled(field);
37
+ for (const tab of field.tabs ?? []) walkFields(tab.fields, {
38
+ disabled: disabled || isDisabled(tab),
39
+ parent: tab.name ? join(scope.parent, tab.name) : scope.parent
40
+ }, context);
41
+ continue;
42
+ }
43
+ if (!field.name) {
44
+ if (field.fields) walkFields(field.fields, {
45
+ disabled: scope.disabled || isDisabled(field),
46
+ parent: scope.parent
47
+ }, context);
48
+ continue;
49
+ }
50
+ const path = join(scope.parent, field.name);
51
+ record(context, path, {
52
+ disabled: scope.disabled || isDisabled(field),
53
+ type: field.type
54
+ });
55
+ const childScope = {
56
+ disabled: false,
57
+ parent: path
58
+ };
59
+ if (field.type === "array" || field.type === "blocks") {
60
+ const rowScope = {
61
+ disabled: false,
62
+ parent: join(path, "*")
63
+ };
64
+ if (field.type === "blocks") for (const block of resolveBlocks(field)) walkBlock(block, rowScope, context);
65
+ if (field.fields) walkFields(field.fields, rowScope, context);
66
+ continue;
67
+ }
68
+ if (field.fields) walkFields(field.fields, childScope, context);
69
+ }
70
+ };
71
+ /**
72
+ * Flatten `fields` into a pattern map.
73
+ *
74
+ * Pass `blocksMap` (the client config's `blocksMap`) whenever the schema may use
75
+ * slug-based `blockReferences`; without it those blocks are skipped, since their
76
+ * definitions live on the config rather than on the field.
77
+ */
78
+ const buildFieldSchemaMap = (fields, options = {}) => {
79
+ const context = {
80
+ blocksMap: options.blocksMap,
81
+ map: /* @__PURE__ */ new Map(),
82
+ openBlocks: /* @__PURE__ */ new Set()
83
+ };
84
+ walkFields(fields, {
85
+ disabled: false,
86
+ parent: ""
87
+ }, context);
88
+ return context.map;
89
+ };
90
+ /**
91
+ * The patterns the history should ignore, from both opt-out sources: fields
92
+ * marked through `admin.custom`, and whole field types the host excluded.
93
+ *
94
+ * Container patterns already cover their subtree (see pathPatterns), so an
95
+ * opted-out group or array needs no entry for its children.
96
+ */
97
+ const collectIgnorePatterns = (map, ignoreFieldTypes = []) => {
98
+ const types = new Set(ignoreFieldTypes);
99
+ const patterns = [];
100
+ for (const entry of map.values()) if (entry.disabled || entry.types.some((type) => types.has(type))) patterns.push(entry.pattern);
101
+ return patterns;
102
+ };
103
+ /**
104
+ * The patterns whose field type is *unambiguously* `type`.
105
+ *
106
+ * Ambiguous patterns are left out, the opposite of how `collectIgnorePatterns`
107
+ * resolves them. Over-matching is the safe direction for an opt-out, because
108
+ * the cost is a field the host did not ask to exclude staying out of the
109
+ * history. It is the unsafe direction here: callers use this to decide how to
110
+ * read a *value*, and applying a JSON field's reading to a text field that
111
+ * shares its path pattern in a sibling block would quietly stop that text field
112
+ * from being captured.
113
+ */
114
+ const collectPatternsOfType = (map, type) => {
115
+ const patterns = [];
116
+ for (const entry of map.values()) if (entry.type === type) patterns.push(entry.pattern);
117
+ return patterns;
118
+ };
119
+ //#endregion
120
+ export { buildFieldSchemaMap, collectIgnorePatterns, collectPatternsOfType };
121
+
122
+ //# sourceMappingURL=fieldSchema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fieldSchema.js","names":[],"sources":["../../src/schema/fieldSchema.ts"],"sourcesContent":["import { UNDO_REDO_FIELD_KEY, type UndoRedoFieldConfig } from './fieldConfig'\n\n/**\n * Walks a Payload field schema and flattens it into the path patterns the\n * history matcher speaks, annotating each with the field's type and whether the\n * host opted it out of undo/redo.\n *\n * This exists because form state carries no type information: `FormField` has a\n * `fieldSchema` member, but only when `buildFormState` is called with\n * `includeSchema: true`, which the document edit view never does. The client\n * config is the only place a mounted component can learn that `layout.0.body`\n * is rich text rather than a plain string.\n *\n * Row indexes collapse to `*`, because a schema describes every row at once:\n * an array `list` containing `title` yields `list.*.title`, which the matcher\n * then tests against the concrete `list.0.title`.\n */\n\n/** Anything that can carry this plugin's per-field opt-out. */\ninterface CustomCarrier {\n\tadmin?: {\n\t\tcustom?: Record<string, unknown>\n\t}\n}\n\n/** The structural shape this walk needs, satisfied by both `Field` and `ClientField`. */\nexport interface WalkableField extends CustomCarrier {\n\ttype: string\n\tname?: string\n\tfields?: WalkableField[]\n\tblocks?: (WalkableBlock | string)[]\n\tblockReferences?: (WalkableBlock | string)[]\n\ttabs?: WalkableTab[]\n}\n\nexport interface WalkableBlock {\n\tslug: string\n\tfields: WalkableField[]\n}\n\nexport interface WalkableTab extends CustomCarrier {\n\tname?: string\n\tfields: WalkableField[]\n}\n\nexport interface FieldSchemaEntry {\n\t/** Form-state path pattern, with `*` standing in for every array or blocks row index. */\n\tpattern: string\n\t/**\n\t * The field's type, or `undefined` when the pattern is ambiguous: two blocks\n\t * in one blocks field can declare the same field name with different types,\n\t * and a form-state path carries no block discriminator to tell them apart.\n\t * Callers must treat `undefined` as \"unknown\" and fall back to inspecting the\n\t * value, never as \"no field here\".\n\t */\n\ttype: string | undefined\n\t/** Every type seen at this pattern, so ambiguity is inspectable rather than silent. */\n\ttypes: string[]\n\t/**\n\t * Whether any contributor marked the field opted out. Ambiguous patterns\n\t * resolve by OR: honouring an opt-out that was meant for a sibling block is\n\t * the safe direction, ignoring one the host asked for is not.\n\t */\n\tdisabled: boolean\n}\n\nexport type FieldSchemaMap = Map<string, FieldSchemaEntry>\n\n/**\n * Guards against a blocks field that reaches itself through a slug reference,\n * which Payload allows and which is the main reason `blockReferences` exists.\n * Recursion is bounded by the set of block slugs already open on the current\n * branch, so a self-referencing block contributes its fields exactly once.\n */\ntype WalkContext = {\n\tblocksMap: Record<string, WalkableBlock> | undefined\n\tmap: FieldSchemaMap\n\topenBlocks: Set<string>\n}\n\n/**\n * Where the walk currently is: the path prefix, and whether an ancestor opted\n * its subtree out *without leaving a pattern behind*.\n *\n * That qualifier is the whole reason the flag exists. A named group or array\n * records a pattern of its own, and a pattern covers its whole subtree, so an\n * opt-out there needs no help travelling downwards. Tabs and unnamed rows,\n * collapsibles and groups contribute no path segment and therefore no pattern,\n * so theirs would be lost unless it is carried to the first descendant that\n * does record one.\n */\ninterface WalkScope {\n\tparent: string\n\tdisabled: boolean\n}\n\nconst readFieldConfig = (field: CustomCarrier): UndoRedoFieldConfig | undefined =>\n\tfield.admin?.custom?.[UNDO_REDO_FIELD_KEY] as UndoRedoFieldConfig | undefined\n\nconst isDisabled = (field: CustomCarrier): boolean => readFieldConfig(field)?.disabled === true\n\nconst record = (\n\tcontext: WalkContext,\n\tpattern: string,\n\tfound: { disabled: boolean; type: string }\n): void => {\n\tconst existing = context.map.get(pattern)\n\tif (!existing) {\n\t\tcontext.map.set(pattern, {\n\t\t\tpattern,\n\t\t\ttype: found.type,\n\t\t\ttypes: [found.type],\n\t\t\tdisabled: found.disabled,\n\t\t})\n\t\treturn\n\t}\n\tif (!existing.types.includes(found.type)) {\n\t\texisting.types.push(found.type)\n\t\texisting.type = undefined\n\t}\n\texisting.disabled ||= found.disabled\n}\n\nconst join = (parent: string, segment: string): string =>\n\tparent === '' ? segment : `${parent}.${segment}`\n\n/** `blockReferences` wins when present: Payload populates one or the other. */\nconst resolveBlocks = (field: WalkableField): (WalkableBlock | string)[] =>\n\tfield.blockReferences ?? field.blocks ?? []\n\nconst walkBlock = (block: WalkableBlock | string, scope: WalkScope, context: WalkContext): void => {\n\tconst resolved = typeof block === 'string' ? context.blocksMap?.[block] : block\n\t// An unresolvable slug means the caller had no blocks map. Skipping keeps the\n\t// rest of the schema usable rather than throwing over one missing definition.\n\tif (!resolved || context.openBlocks.has(resolved.slug)) return\n\tcontext.openBlocks.add(resolved.slug)\n\twalkFields(resolved.fields, scope, context)\n\tcontext.openBlocks.delete(resolved.slug)\n}\n\nconst walkFields = (fields: WalkableField[], scope: WalkScope, context: WalkContext): void => {\n\tfor (const field of fields) {\n\t\t// UI fields never reach the data, so they never appear in form state.\n\t\tif (field.type === 'ui') continue\n\n\t\tif (field.type === 'tabs') {\n\t\t\tconst disabled = scope.disabled || isDisabled(field)\n\t\t\tfor (const tab of field.tabs ?? []) {\n\t\t\t\t// A named tab adds a segment; an unnamed one leaves its children\n\t\t\t\t// where they were, exactly like a row or an unnamed group. Neither\n\t\t\t\t// records a pattern, so an opt-out on the tabs field or on the tab\n\t\t\t\t// itself has to continue downwards.\n\t\t\t\twalkFields(\n\t\t\t\t\ttab.fields,\n\t\t\t\t\t{\n\t\t\t\t\t\tdisabled: disabled || isDisabled(tab),\n\t\t\t\t\t\tparent: tab.name ? join(scope.parent, tab.name) : scope.parent,\n\t\t\t\t\t},\n\t\t\t\t\tcontext\n\t\t\t\t)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// Rows, collapsibles and unnamed groups are presentational: Payload gives\n\t\t// them an `_index-N` path of their own but strips it again for their\n\t\t// children, so they contribute nothing here beyond their opt-out.\n\t\tif (!field.name) {\n\t\t\tif (field.fields) {\n\t\t\t\twalkFields(\n\t\t\t\t\tfield.fields,\n\t\t\t\t\t{ disabled: scope.disabled || isDisabled(field), parent: scope.parent },\n\t\t\t\t\tcontext\n\t\t\t\t)\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tconst path = join(scope.parent, field.name)\n\t\trecord(context, path, { disabled: scope.disabled || isDisabled(field), type: field.type })\n\t\t// This field now has a pattern of its own, which covers everything below\n\t\t// it, so the inherited flag has done its job and stops here rather than\n\t\t// producing a redundant entry per descendant.\n\t\tconst childScope = { disabled: false, parent: path }\n\n\t\tif (field.type === 'array' || field.type === 'blocks') {\n\t\t\tconst rowScope = { disabled: false, parent: join(path, '*') }\n\t\t\tif (field.type === 'blocks') {\n\t\t\t\tfor (const block of resolveBlocks(field)) {\n\t\t\t\t\twalkBlock(block, rowScope, context)\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (field.fields) walkFields(field.fields, rowScope, context)\n\t\t\tcontinue\n\t\t}\n\n\t\tif (field.fields) walkFields(field.fields, childScope, context)\n\t}\n}\n\n/**\n * Flatten `fields` into a pattern map.\n *\n * Pass `blocksMap` (the client config's `blocksMap`) whenever the schema may use\n * slug-based `blockReferences`; without it those blocks are skipped, since their\n * definitions live on the config rather than on the field.\n */\nexport const buildFieldSchemaMap = (\n\tfields: WalkableField[],\n\toptions: { blocksMap?: Record<string, WalkableBlock> } = {}\n): FieldSchemaMap => {\n\tconst context: WalkContext = {\n\t\tblocksMap: options.blocksMap,\n\t\tmap: new Map(),\n\t\topenBlocks: new Set(),\n\t}\n\twalkFields(fields, { disabled: false, parent: '' }, context)\n\treturn context.map\n}\n\n/**\n * The patterns the history should ignore, from both opt-out sources: fields\n * marked through `admin.custom`, and whole field types the host excluded.\n *\n * Container patterns already cover their subtree (see pathPatterns), so an\n * opted-out group or array needs no entry for its children.\n */\nexport const collectIgnorePatterns = (\n\tmap: FieldSchemaMap,\n\tignoreFieldTypes: readonly string[] = []\n): string[] => {\n\tconst types = new Set(ignoreFieldTypes)\n\tconst patterns: string[] = []\n\tfor (const entry of map.values()) {\n\t\tif (entry.disabled || entry.types.some((type) => types.has(type))) {\n\t\t\tpatterns.push(entry.pattern)\n\t\t}\n\t}\n\treturn patterns\n}\n\n/**\n * The patterns whose field type is *unambiguously* `type`.\n *\n * Ambiguous patterns are left out, the opposite of how `collectIgnorePatterns`\n * resolves them. Over-matching is the safe direction for an opt-out, because\n * the cost is a field the host did not ask to exclude staying out of the\n * history. It is the unsafe direction here: callers use this to decide how to\n * read a *value*, and applying a JSON field's reading to a text field that\n * shares its path pattern in a sibling block would quietly stop that text field\n * from being captured.\n */\nexport const collectPatternsOfType = (map: FieldSchemaMap, type: string): string[] => {\n\tconst patterns: string[] = []\n\tfor (const entry of map.values()) {\n\t\tif (entry.type === type) patterns.push(entry.pattern)\n\t}\n\treturn patterns\n}\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { keys } from "./keys.js";
2
+ //#region src/translations/de.ts
3
+ /** German values, keyed by the typed constants in `keys.ts`. */
4
+ const de = {
5
+ [keys.undo]: "Rückgängig",
6
+ [keys.redo]: "Wiederholen",
7
+ [keys.debug]: "Rückgängig-Verlauf",
8
+ [keys.debugTooltip]: "Rückgängig-Verlauf untersuchen",
9
+ [keys.debugTitle]: "Rückgängig-Verlauf",
10
+ [keys.debugEmpty]: "Noch kein Verlauf erfasst.",
11
+ [keys.debugClose]: "Schließen",
12
+ [keys.debugPending]: "Ausstehend (nicht erfasst)",
13
+ [keys.debugOriginal]: "Ursprungszustand",
14
+ [keys.debugCopy]: "JSON kopieren"
15
+ };
16
+ //#endregion
17
+ export { de };
18
+
19
+ //# sourceMappingURL=de.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"de.js","names":[],"sources":["../../src/translations/de.ts"],"sourcesContent":["import { keys, type TranslationKey } from './keys'\n\n/** German values, keyed by the typed constants in `keys.ts`. */\nexport const de: Record<TranslationKey, string> = {\n\t[keys.undo]: 'Rückgängig',\n\t[keys.redo]: 'Wiederholen',\n\t[keys.debug]: 'Rückgängig-Verlauf',\n\t[keys.debugTooltip]: 'Rückgängig-Verlauf untersuchen',\n\t[keys.debugTitle]: 'Rückgängig-Verlauf',\n\t[keys.debugEmpty]: 'Noch kein Verlauf erfasst.',\n\t[keys.debugClose]: 'Schließen',\n\t[keys.debugPending]: 'Ausstehend (nicht erfasst)',\n\t[keys.debugOriginal]: 'Ursprungszustand',\n\t[keys.debugCopy]: 'JSON kopieren',\n}\n"],"mappings":";;;AAGA,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"}