@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.
- package/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +54 -0
- package/dist/client/HistoryDebugOverlay.d.ts +30 -0
- package/dist/client/HistoryDebugOverlay.js +201 -0
- package/dist/client/HistoryDebugOverlay.js.map +1 -0
- package/dist/client/UndoRedoControls.d.ts +18 -0
- package/dist/client/UndoRedoControls.js +155 -0
- package/dist/client/UndoRedoControls.js.map +1 -0
- package/dist/client/formatShortcut.js +113 -0
- package/dist/client/formatShortcut.js.map +1 -0
- package/dist/client/historyDebugOverlay.css +206 -0
- package/dist/client/undoRedoControls.css +58 -0
- package/dist/client/useUndoRedo.d.ts +70 -0
- package/dist/client/useUndoRedo.js +259 -0
- package/dist/client/useUndoRedo.js.map +1 -0
- package/dist/exports/client.d.ts +4 -0
- package/dist/exports/client.js +5 -0
- package/dist/exports/i18n.d.ts +3 -0
- package/dist/exports/i18n.js +3 -0
- package/dist/exports/types.d.ts +5 -0
- package/dist/exports/types.js +1 -0
- package/dist/history/historyCore.d.ts +85 -0
- package/dist/history/historyCore.js +318 -0
- package/dist/history/historyCore.js.map +1 -0
- package/dist/history/pathPatterns.d.ts +6 -0
- package/dist/history/pathPatterns.js +45 -0
- package/dist/history/pathPatterns.js.map +1 -0
- package/dist/history/volatileValues.js +51 -0
- package/dist/history/volatileValues.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +28 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin/options.d.ts +109 -0
- package/dist/plugin/options.js +52 -0
- package/dist/plugin/options.js.map +1 -0
- package/dist/plugin/registerTranslations.js +19 -0
- package/dist/plugin/registerTranslations.js.map +1 -0
- package/dist/plugin/withUndoRedo.d.ts +34 -0
- package/dist/plugin/withUndoRedo.js +72 -0
- package/dist/plugin/withUndoRedo.js.map +1 -0
- package/dist/schema/fieldConfig.d.ts +36 -0
- package/dist/schema/fieldConfig.js +28 -0
- package/dist/schema/fieldConfig.js.map +1 -0
- package/dist/schema/fieldSchema.d.ts +63 -0
- package/dist/schema/fieldSchema.js +122 -0
- package/dist/schema/fieldSchema.js.map +1 -0
- package/dist/translations/de.js +19 -0
- package/dist/translations/de.js.map +1 -0
- package/dist/translations/en.js +23 -0
- package/dist/translations/en.js.map +1 -0
- package/dist/translations/index.d.ts +15 -0
- package/dist/translations/index.js +30 -0
- package/dist/translations/index.js.map +1 -0
- package/dist/translations/keys.d.ts +22 -0
- package/dist/translations/keys.js +22 -0
- package/dist/translations/keys.js.map +1 -0
- package/dist/translations/useTranslation.js +12 -0
- package/dist/translations/useTranslation.js.map +1 -0
- package/package.json +109 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { createPathMatcher } from "./pathPatterns.js";
|
|
2
|
+
//#region src/history/historyCore.ts
|
|
3
|
+
/**
|
|
4
|
+
* Pure, React-free core of the admin undo/redo feature.
|
|
5
|
+
*
|
|
6
|
+
* The admin form keeps its state in a flat `FormState` map (path → FieldState).
|
|
7
|
+
* We snapshot that map on (debounced) user edits and restore old snapshots via
|
|
8
|
+
* the form's `REPLACE_STATE` reducer action. Field states carry non-serializable
|
|
9
|
+
* members (React nodes in `customComponents`, `validate` functions), so snapshots
|
|
10
|
+
* shallow-copy each field state and keep those members by reference: only the
|
|
11
|
+
* top level is protected against later mutation, which matches how the form
|
|
12
|
+
* reducer itself treats field states (immutable-style replacement).
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Payload's own fields, which no editor edits directly and undo must never
|
|
16
|
+
* touch. They change on save, autosave, publish and token refresh without user
|
|
17
|
+
* interaction, so treating them as edits would create "phantom" history entries
|
|
18
|
+
* where undo appears to do nothing, and restoring a stale value could revert a
|
|
19
|
+
* publish (`_status`) or corrupt auth state (`sessions`, `salt`, `hash`).
|
|
20
|
+
*
|
|
21
|
+
* Deliberately limited to fields Payload itself injects. Derived fields that a
|
|
22
|
+
* plugin or project adds are user-facing (a project may well have its own
|
|
23
|
+
* `pathname`), so excluding those is the host's call through `ignorePaths`
|
|
24
|
+
* rather than a default baked in here.
|
|
25
|
+
*
|
|
26
|
+
* Each entry ignores its whole subtree, which is what the pattern matcher does
|
|
27
|
+
* for a pattern that runs out before the path does.
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_IGNORED_PATHS = [
|
|
30
|
+
"_status",
|
|
31
|
+
"createdAt",
|
|
32
|
+
"hash",
|
|
33
|
+
"lockUntil",
|
|
34
|
+
"loginAttempts",
|
|
35
|
+
"resetPasswordExpiration",
|
|
36
|
+
"resetPasswordToken",
|
|
37
|
+
"salt",
|
|
38
|
+
"sessions",
|
|
39
|
+
"updatedAt"
|
|
40
|
+
];
|
|
41
|
+
/** True for paths the undo history ignores entirely (see DEFAULT_IGNORED_PATHS). */
|
|
42
|
+
const isIgnoredPath = createPathMatcher(DEFAULT_IGNORED_PATHS);
|
|
43
|
+
const DEFAULT_HISTORY_OPTIONS = {
|
|
44
|
+
isIgnored: isIgnoredPath,
|
|
45
|
+
isVolatile: () => false,
|
|
46
|
+
maxHistory: 50
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Structural deep equality for JSON-ish data (objects, arrays, primitives).
|
|
50
|
+
* Key order is irrelevant; `undefined` properties equal missing properties.
|
|
51
|
+
*/
|
|
52
|
+
const deepEqual = (a, b) => {
|
|
53
|
+
if (a === b) return true;
|
|
54
|
+
if (typeof a === "number" && typeof b === "number") return Number.isNaN(a) && Number.isNaN(b);
|
|
55
|
+
if (a == null || b == null) return a === b;
|
|
56
|
+
if (typeof a !== "object" || typeof b !== "object") return false;
|
|
57
|
+
if (Array.isArray(a) !== Array.isArray(b)) return false;
|
|
58
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
59
|
+
if (a.length !== b.length) return false;
|
|
60
|
+
return a.every((item, i) => deepEqual(item, b[i]));
|
|
61
|
+
}
|
|
62
|
+
const aObj = a;
|
|
63
|
+
const bObj = b;
|
|
64
|
+
const keys = new Set([...Object.keys(aObj), ...Object.keys(bObj)]);
|
|
65
|
+
for (const key of keys) if (!deepEqual(aObj[key], bObj[key])) return false;
|
|
66
|
+
return true;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* True for one reference of a polymorphic relationship or upload.
|
|
70
|
+
*
|
|
71
|
+
* Deliberately structural. The alternative, asking the schema for the field's
|
|
72
|
+
* type, cannot answer for a path that two blocks declare differently, and this
|
|
73
|
+
* needs no such tie-break: the shape it matches, a `relationTo` slug next to an
|
|
74
|
+
* id, is what Payload writes for exactly these fields.
|
|
75
|
+
*/
|
|
76
|
+
const isPolymorphicRef = (value) => {
|
|
77
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
78
|
+
const ref = value;
|
|
79
|
+
return typeof ref.relationTo === "string" && (typeof ref.value === "string" || typeof ref.value === "number");
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Reduce a polymorphic reference to the reference itself.
|
|
83
|
+
*
|
|
84
|
+
* A polymorphic relationship hands react-select's own option objects straight
|
|
85
|
+
* to form state rather than mapping them down to ids the way a single-target
|
|
86
|
+
* one does (see @payloadcms/ui fields/Relationship, `dataToSet`), so the value
|
|
87
|
+
* carries `label` and `allowEdit` alongside `relationTo` and `value`. Those
|
|
88
|
+
* move without anyone editing anything: a label refreshes when the related
|
|
89
|
+
* document is saved, `allowEdit` appears once permissions resolve, and the
|
|
90
|
+
* server merge after a save replaces the whole option with the bare reference.
|
|
91
|
+
* The save case is the visible one, since it appends an entry identical to the
|
|
92
|
+
* one before it, which then costs an extra undo to get past.
|
|
93
|
+
*
|
|
94
|
+
* Payload draws the same line: its own "did this change" test for polymorphic
|
|
95
|
+
* values compares `value` and `relationTo` and nothing else.
|
|
96
|
+
*/
|
|
97
|
+
const toReference = (value) => isPolymorphicRef(value) ? {
|
|
98
|
+
relationTo: value.relationTo,
|
|
99
|
+
value: value.value
|
|
100
|
+
} : value;
|
|
101
|
+
const normalizeComparableValue = (value) => Array.isArray(value) ? value.map(toReference) : toReference(value);
|
|
102
|
+
/**
|
|
103
|
+
* Reduce a field state to what counts as a user-visible edit.
|
|
104
|
+
*
|
|
105
|
+
* For array and blocks fields the row ids are the whole story, and `value` is
|
|
106
|
+
* deliberately dropped: Payload sets it to the row count, which `rowIds` already
|
|
107
|
+
* encodes, and uses it as a "no data" marker in a way that changes without any
|
|
108
|
+
* edit. A localized blocks field with no value in the active locale is built
|
|
109
|
+
* with `value: null`, and the next form-state build, triggered by an edit to
|
|
110
|
+
* some unrelated field, rewrites it to `0` (arrays do the same between
|
|
111
|
+
* `undefined` and `0`, see @payloadcms/ui addFieldStatePromise).
|
|
112
|
+
*
|
|
113
|
+
* Comparing that would report an edit nobody made. Worse, the phantom lands on
|
|
114
|
+
* the capture that runs at the start of the next restore, which appends an
|
|
115
|
+
* entry and truncates the redo tail, so redo silently stops working after the
|
|
116
|
+
* first undo.
|
|
117
|
+
*/
|
|
118
|
+
const extractComparableField = (field) => field.rows ? { rowIds: field.rows.map((row) => row.id) } : { value: normalizeComparableValue(field.value) };
|
|
119
|
+
const extractComparable = (fields, isIgnored = isIgnoredPath) => {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const [path, field] of Object.entries(fields)) {
|
|
122
|
+
if (isIgnored(path) || !field) continue;
|
|
123
|
+
out[path] = extractComparableField(field);
|
|
124
|
+
}
|
|
125
|
+
return out;
|
|
126
|
+
};
|
|
127
|
+
let nextEntryId = 0;
|
|
128
|
+
const createSnapshot = (fields, isIgnored = isIgnoredPath) => ({
|
|
129
|
+
id: nextEntryId++,
|
|
130
|
+
fields: Object.fromEntries(Object.entries(fields).map(([path, field]) => [path, { ...field }])),
|
|
131
|
+
comparable: extractComparable(fields, isIgnored)
|
|
132
|
+
});
|
|
133
|
+
const createHistory = (options = {}) => ({
|
|
134
|
+
stack: [],
|
|
135
|
+
index: -1,
|
|
136
|
+
savedComparable: null,
|
|
137
|
+
options: {
|
|
138
|
+
...DEFAULT_HISTORY_OPTIONS,
|
|
139
|
+
...options
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
/**
|
|
143
|
+
* Record `fields` as the persisted state. Call whenever the form becomes clean:
|
|
144
|
+
* a save, an autosave, a reset, the initial load.
|
|
145
|
+
*
|
|
146
|
+
* Reads the live form state rather than the stack, so a save made with edits
|
|
147
|
+
* still inside the capture debounce is recorded accurately, and does not append
|
|
148
|
+
* an entry, so it can never disturb the redo tail.
|
|
149
|
+
*/
|
|
150
|
+
const markSaved = (history, fields) => {
|
|
151
|
+
history.savedComparable = projectComparable(history, fields);
|
|
152
|
+
};
|
|
153
|
+
/** True when the form matches the persisted document, so it can be reported clean. */
|
|
154
|
+
const isAtSavedState = (history) => {
|
|
155
|
+
if (!history.savedComparable) return false;
|
|
156
|
+
const current = history.stack[history.index];
|
|
157
|
+
return current !== void 0 && deepEqual(current.comparable, history.savedComparable);
|
|
158
|
+
};
|
|
159
|
+
/**
|
|
160
|
+
* What a volatile path contributes to a snapshot: whatever the previous entry
|
|
161
|
+
* recorded for it, or, with no entry to carry from, the live field reverted to
|
|
162
|
+
* its `initialValue`, which is the persisted value and therefore a state the
|
|
163
|
+
* document really was in.
|
|
164
|
+
*/
|
|
165
|
+
const carryForward = (path, live, previous) => {
|
|
166
|
+
const carried = previous?.fields[path];
|
|
167
|
+
if (carried) return {
|
|
168
|
+
comparable: previous?.comparable[path] ?? extractComparableField(carried),
|
|
169
|
+
field: carried
|
|
170
|
+
};
|
|
171
|
+
const reverted = {
|
|
172
|
+
...live,
|
|
173
|
+
value: live.initialValue
|
|
174
|
+
};
|
|
175
|
+
return {
|
|
176
|
+
comparable: extractComparableField(reverted),
|
|
177
|
+
field: reverted
|
|
178
|
+
};
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Every volatile, non-ignored path of `fields`, mapped to what it carries
|
|
182
|
+
* forward from the entry the form currently shows.
|
|
183
|
+
*/
|
|
184
|
+
const collectCarried = (history, fields) => {
|
|
185
|
+
const { isIgnored, isVolatile } = history.options;
|
|
186
|
+
const previous = history.stack[history.index];
|
|
187
|
+
const out = /* @__PURE__ */ new Map();
|
|
188
|
+
for (const [path, field] of Object.entries(fields)) {
|
|
189
|
+
if (!field || isIgnored(path) || !isVolatile(path, field)) continue;
|
|
190
|
+
out.set(path, carryForward(path, field, previous));
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
};
|
|
194
|
+
/**
|
|
195
|
+
* The comparable state a capture of `fields` would record right now, carry
|
|
196
|
+
* forward included.
|
|
197
|
+
*
|
|
198
|
+
* The debug overlay diffs live form state against this rather than against a
|
|
199
|
+
* plain extract, so a value that can never be captured does not show up as a
|
|
200
|
+
* change that is forever pending.
|
|
201
|
+
*/
|
|
202
|
+
const projectComparable = (history, fields) => {
|
|
203
|
+
const out = extractComparable(fields, history.options.isIgnored);
|
|
204
|
+
for (const [path, carried] of collectCarried(history, fields)) out[path] = carried.comparable;
|
|
205
|
+
return out;
|
|
206
|
+
};
|
|
207
|
+
/**
|
|
208
|
+
* Push the current form state onto the history. No-ops (and returns false) when
|
|
209
|
+
* nothing user-visible changed relative to the entry at the current index,
|
|
210
|
+
* which absorbs server-merge echoes after saves/restores. A real change drops
|
|
211
|
+
* the redo tail, appends, and caps the stack at the history's `maxHistory`.
|
|
212
|
+
*/
|
|
213
|
+
const pushSnapshot = (history, fields) => {
|
|
214
|
+
const { isIgnored, maxHistory } = history.options;
|
|
215
|
+
const snapshot = createSnapshot(fields, isIgnored);
|
|
216
|
+
const current = history.stack[history.index];
|
|
217
|
+
for (const [path, carried] of collectCarried(history, fields)) {
|
|
218
|
+
snapshot.fields[path] = carried.field;
|
|
219
|
+
snapshot.comparable[path] = carried.comparable;
|
|
220
|
+
}
|
|
221
|
+
if (current && deepEqual(current.comparable, snapshot.comparable)) return false;
|
|
222
|
+
history.stack.splice(history.index + 1);
|
|
223
|
+
history.stack.push(snapshot);
|
|
224
|
+
if (history.stack.length > maxHistory) history.stack.splice(0, history.stack.length - maxHistory);
|
|
225
|
+
history.index = history.stack.length - 1;
|
|
226
|
+
return true;
|
|
227
|
+
};
|
|
228
|
+
/**
|
|
229
|
+
* Path-level changes from one comparable state to another, sorted by path.
|
|
230
|
+
* Powers the debug overlay and makes "what did this history entry capture?"
|
|
231
|
+
* answerable without diffing whole FormState objects by eye.
|
|
232
|
+
*/
|
|
233
|
+
const diffComparable = (from, to) => {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const path of new Set([...Object.keys(from), ...Object.keys(to)])) {
|
|
236
|
+
const a = from[path];
|
|
237
|
+
const b = to[path];
|
|
238
|
+
if (a && b && deepEqual(a.value, b.value) && deepEqual(a.rowIds, b.rowIds)) continue;
|
|
239
|
+
const diff = {
|
|
240
|
+
path,
|
|
241
|
+
from: a?.value,
|
|
242
|
+
to: b?.value
|
|
243
|
+
};
|
|
244
|
+
if (a?.rowIds || b?.rowIds) {
|
|
245
|
+
diff.fromRowIds = a?.rowIds;
|
|
246
|
+
diff.toRowIds = b?.rowIds;
|
|
247
|
+
}
|
|
248
|
+
if (!a) diff.presence = "added";
|
|
249
|
+
else if (!b) diff.presence = "removed";
|
|
250
|
+
out.push(diff);
|
|
251
|
+
}
|
|
252
|
+
return out.sort((x, y) => x.path.localeCompare(y.path));
|
|
253
|
+
};
|
|
254
|
+
const canUndo = (history) => history.index > 0;
|
|
255
|
+
const canRedo = (history) => history.index >= 0 && history.index < history.stack.length - 1;
|
|
256
|
+
/** Lexical editor state values have the shape `{ root: { children: [...] } }`. */
|
|
257
|
+
const isLexicalValue = (value) => {
|
|
258
|
+
if (value == null || typeof value !== "object") return false;
|
|
259
|
+
const root = value.root;
|
|
260
|
+
return root != null && typeof root === "object" && "children" in root;
|
|
261
|
+
};
|
|
262
|
+
const cloneJson = (value) => value == null ? value : JSON.parse(JSON.stringify(value));
|
|
263
|
+
/**
|
|
264
|
+
* A value that means the same as `live` but is never `Object.is` to it.
|
|
265
|
+
*
|
|
266
|
+
* Lexical only re-initializes a mounted editor when the `initialValue` it is
|
|
267
|
+
* handed is a different *reference* from the one it last saw, so restoring has
|
|
268
|
+
* to hand it a new one. Cloning covers that for an editor state object, but not
|
|
269
|
+
* for a rich text field that was never saved, whose initialValue is nullish: a
|
|
270
|
+
* primitive cannot be cloned into a fresh identity, and `null` re-sent as
|
|
271
|
+
* `null` looks unchanged.
|
|
272
|
+
*
|
|
273
|
+
* Undo appeared to work there because it swapped `undefined` for `null`, which
|
|
274
|
+
* is an identity change once. Every restore after it kept sending `null` and
|
|
275
|
+
* the editor stayed on screen holding stale content, even though the form value
|
|
276
|
+
* underneath had been restored, which is why saving made the change appear.
|
|
277
|
+
* Alternating between the two nullish values keeps every restore distinguishable
|
|
278
|
+
* while leaving the meaning, "this field started out empty", untouched.
|
|
279
|
+
*/
|
|
280
|
+
const withNewIdentity = (live) => {
|
|
281
|
+
if (live === null) return void 0;
|
|
282
|
+
if (live === void 0) return null;
|
|
283
|
+
return cloneJson(live);
|
|
284
|
+
};
|
|
285
|
+
/**
|
|
286
|
+
* Build the FormState to dispatch via REPLACE_STATE when restoring `snapshot`.
|
|
287
|
+
*
|
|
288
|
+
* - `initialValue` is taken from the live state, not the snapshot, so Payload's
|
|
289
|
+
* modified/"leave without saving" detection stays truthful after save cycles.
|
|
290
|
+
* - Changed fields get `isModified: true`, which stops a stale in-flight
|
|
291
|
+
* autosave response from overwriting the restored value during the server
|
|
292
|
+
* form-state merge (local modified fields win in `mergeServerFormState`).
|
|
293
|
+
* - Changed rich text fields additionally get a *new* `initialValue` reference:
|
|
294
|
+
* the mounted Lexical editor only re-initializes from the form value when the
|
|
295
|
+
* initialValue reference changes (see @payloadcms/richtext-lexical Field.tsx).
|
|
296
|
+
*/
|
|
297
|
+
const buildRestoreState = (snapshot, currentFields, isIgnored = isIgnoredPath) => {
|
|
298
|
+
const out = {};
|
|
299
|
+
for (const [path, snapField] of Object.entries(snapshot.fields)) {
|
|
300
|
+
if (isIgnored(path)) continue;
|
|
301
|
+
const cur = currentFields[path];
|
|
302
|
+
const snapComparable = snapshot.comparable[path] ?? extractComparableField(snapField);
|
|
303
|
+
const changed = !cur || !deepEqual(snapComparable, extractComparableField(cur));
|
|
304
|
+
const restored = { ...snapField };
|
|
305
|
+
if (cur) restored.initialValue = cur.initialValue;
|
|
306
|
+
if (changed) {
|
|
307
|
+
restored.isModified = true;
|
|
308
|
+
if (isLexicalValue(snapField.value) || isLexicalValue(cur?.value)) restored.initialValue = withNewIdentity(cur?.initialValue);
|
|
309
|
+
}
|
|
310
|
+
out[path] = restored;
|
|
311
|
+
}
|
|
312
|
+
for (const [path, curField] of Object.entries(currentFields)) if (isIgnored(path)) out[path] = curField;
|
|
313
|
+
return out;
|
|
314
|
+
};
|
|
315
|
+
//#endregion
|
|
316
|
+
export { DEFAULT_IGNORED_PATHS, buildRestoreState, canRedo, canUndo, createHistory, deepEqual, diffComparable, isAtSavedState, markSaved, projectComparable, pushSnapshot };
|
|
317
|
+
|
|
318
|
+
//# sourceMappingURL=historyCore.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
//#region src/history/pathPatterns.ts
|
|
2
|
+
/**
|
|
3
|
+
* Path pattern matching for the undo history's ignore list.
|
|
4
|
+
*
|
|
5
|
+
* Form-state paths are dot separated and carry a numeric segment for every
|
|
6
|
+
* array or blocks row (`list.0.nested.1.value`), so a pattern language needs
|
|
7
|
+
* exactly one wildcard: `*`, matching a single segment. That covers "this field
|
|
8
|
+
* in every row" (`list.*.rowRich`) without needing a second, greedier wildcard,
|
|
9
|
+
* because a pattern that runs out before the path does matches the whole
|
|
10
|
+
* subtree below it.
|
|
11
|
+
*
|
|
12
|
+
* Subtree semantics are the point, not a side effect: ignoring an array by name
|
|
13
|
+
* (`list`) has to ignore its rows too, otherwise the rows keep producing history
|
|
14
|
+
* entries for a field the host asked to be left alone.
|
|
15
|
+
*/
|
|
16
|
+
const WILDCARD = "*";
|
|
17
|
+
const matchesSegments = (pathSegments, patternSegments) => {
|
|
18
|
+
if (patternSegments.length > pathSegments.length) return false;
|
|
19
|
+
return patternSegments.every((segment, i) => segment === WILDCARD || segment === pathSegments[i]);
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Build a matcher over `patterns`.
|
|
23
|
+
*
|
|
24
|
+
* Patterns are split once up front and verdicts are memoized per path, because
|
|
25
|
+
* this runs over every path in the form state on every capture, and the set of
|
|
26
|
+
* paths a document produces is bounded by its schema rather than by session
|
|
27
|
+
* length. An empty pattern set short-circuits to a constant `false`.
|
|
28
|
+
*/
|
|
29
|
+
const createPathMatcher = (patterns) => {
|
|
30
|
+
const compiled = [...new Set(patterns)].filter((pattern) => pattern.length > 0).map((pattern) => pattern.split("."));
|
|
31
|
+
if (compiled.length === 0) return () => false;
|
|
32
|
+
const memo = /* @__PURE__ */ new Map();
|
|
33
|
+
return (path) => {
|
|
34
|
+
const cached = memo.get(path);
|
|
35
|
+
if (cached !== void 0) return cached;
|
|
36
|
+
const segments = path.split(".");
|
|
37
|
+
const result = compiled.some((pattern) => matchesSegments(segments, pattern));
|
|
38
|
+
memo.set(path, result);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
export { createPathMatcher };
|
|
44
|
+
|
|
45
|
+
//# sourceMappingURL=pathPatterns.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pathPatterns.js","names":[],"sources":["../../src/history/pathPatterns.ts"],"sourcesContent":["/**\n * Path pattern matching for the undo history's ignore list.\n *\n * Form-state paths are dot separated and carry a numeric segment for every\n * array or blocks row (`list.0.nested.1.value`), so a pattern language needs\n * exactly one wildcard: `*`, matching a single segment. That covers \"this field\n * in every row\" (`list.*.rowRich`) without needing a second, greedier wildcard,\n * because a pattern that runs out before the path does matches the whole\n * subtree below it.\n *\n * Subtree semantics are the point, not a side effect: ignoring an array by name\n * (`list`) has to ignore its rows too, otherwise the rows keep producing history\n * entries for a field the host asked to be left alone.\n */\n\nconst WILDCARD = '*'\n\n/**\n * True when `pattern` matches `path` exactly or matches one of its ancestors.\n *\n * `list.*.title` matches `list.0.title` but not `list.0.meta.title`: a `*`\n * stands for one segment, never several.\n */\nexport const matchesPattern = (path: string, pattern: string): boolean =>\n\tmatchesSegments(path.split('.'), pattern.split('.'))\n\nconst matchesSegments = (pathSegments: string[], patternSegments: string[]): boolean => {\n\t// A longer pattern cannot match: it would have to consume segments the path\n\t// does not have. An equal or shorter one matches the path or an ancestor.\n\tif (patternSegments.length > pathSegments.length) return false\n\treturn patternSegments.every((segment, i) => segment === WILDCARD || segment === pathSegments[i])\n}\n\n/** Tests a path against a fixed pattern set. */\nexport type PathMatcher = (path: string) => boolean\n\n/**\n * Build a matcher over `patterns`.\n *\n * Patterns are split once up front and verdicts are memoized per path, because\n * this runs over every path in the form state on every capture, and the set of\n * paths a document produces is bounded by its schema rather than by session\n * length. An empty pattern set short-circuits to a constant `false`.\n */\nexport const createPathMatcher = (patterns: Iterable<string>): PathMatcher => {\n\tconst compiled = [...new Set(patterns)]\n\t\t.filter((pattern) => pattern.length > 0)\n\t\t.map((pattern) => pattern.split('.'))\n\tif (compiled.length === 0) return () => false\n\n\tconst memo = new Map<string, boolean>()\n\treturn (path: string): boolean => {\n\t\tconst cached = memo.get(path)\n\t\tif (cached !== undefined) return cached\n\t\tconst segments = path.split('.')\n\t\tconst result = compiled.some((pattern) => matchesSegments(segments, pattern))\n\t\tmemo.set(path, result)\n\t\treturn result\n\t}\n}\n"],"mappings":";;;;;;;;;;;;;;;AAeA,MAAM,WAAW;AAWjB,MAAM,mBAAmB,cAAwB,oBAAuC;CAGvF,IAAI,gBAAgB,SAAS,aAAa,QAAQ,OAAO;CACzD,OAAO,gBAAgB,OAAO,SAAS,MAAM,YAAY,YAAY,YAAY,aAAa,EAAE;AACjG;;;;;;;;;AAaA,MAAa,qBAAqB,aAA4C;CAC7E,MAAM,WAAW,CAAC,GAAG,IAAI,IAAI,QAAQ,CAAC,EACpC,QAAQ,YAAY,QAAQ,SAAS,CAAC,EACtC,KAAK,YAAY,QAAQ,MAAM,GAAG,CAAC;CACrC,IAAI,SAAS,WAAW,GAAG,aAAa;CAExC,MAAM,uBAAO,IAAI,IAAqB;CACtC,QAAQ,SAA0B;EACjC,MAAM,SAAS,KAAK,IAAI,IAAI;EAC5B,IAAI,WAAW,KAAA,GAAW,OAAO;EACjC,MAAM,WAAW,KAAK,MAAM,GAAG;EAC/B,MAAM,SAAS,SAAS,MAAM,YAAY,gBAAgB,UAAU,OAAO,CAAC;EAC5E,KAAK,IAAI,MAAM,MAAM;EACrB,OAAO;CACR;AACD"}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createPathMatcher } from "./pathPatterns.js";
|
|
2
|
+
import { collectPatternsOfType } from "../schema/fieldSchema.js";
|
|
3
|
+
//#region src/history/volatileValues.ts
|
|
4
|
+
/**
|
|
5
|
+
* Recognising form-state values that are a field mid-edit rather than data, so
|
|
6
|
+
* the history can decline to record a state it would not be able to restore.
|
|
7
|
+
*
|
|
8
|
+
* Only Payload's JSON field needs this today. Every other field type keeps its
|
|
9
|
+
* editing surface and its form value in the same representation, so whatever
|
|
10
|
+
* the value holds can be dispatched straight back.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* True for a value Payload's JSON field is holding as raw editor text rather
|
|
14
|
+
* than as parsed data.
|
|
15
|
+
*
|
|
16
|
+
* The field parses on every keystroke and, when the parse throws, writes the
|
|
17
|
+
* editor's text into form state as a string (see @payloadcms/ui fields/JSON,
|
|
18
|
+
* `handleChange`). A string that is not itself valid JSON therefore came from
|
|
19
|
+
* that branch: it is text the editor has not finished, not a value.
|
|
20
|
+
*
|
|
21
|
+
* A JSON field legitimately holding a bare string is indistinguishable from
|
|
22
|
+
* that, since both arrive as a string that does not parse, and Payload keeps no
|
|
23
|
+
* error flag in form state to tell them apart. Reading it as mid-edit is the
|
|
24
|
+
* harmless direction: the path drops out of the history until it holds
|
|
25
|
+
* something else, which is exactly what an ignored path does.
|
|
26
|
+
*/
|
|
27
|
+
const isUnparsedJson = (value) => {
|
|
28
|
+
if (typeof value !== "string") return false;
|
|
29
|
+
try {
|
|
30
|
+
JSON.parse(value);
|
|
31
|
+
return false;
|
|
32
|
+
} catch {
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Build the history's volatile matcher from a document's schema.
|
|
38
|
+
*
|
|
39
|
+
* An empty map yields a matcher that is never true, which is also what a
|
|
40
|
+
* document whose schema could not be resolved should get: capturing a value the
|
|
41
|
+
* restore may mangle is worse than the alternative, but guessing which paths
|
|
42
|
+
* are JSON without a schema would be worse still.
|
|
43
|
+
*/
|
|
44
|
+
const createVolatileMatcher = (map) => {
|
|
45
|
+
const isJsonPath = createPathMatcher(collectPatternsOfType(map, "json"));
|
|
46
|
+
return (path, field) => isJsonPath(path) && isUnparsedJson(field.value);
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { createVolatileMatcher };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=volatileValues.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"volatileValues.js","names":[],"sources":["../../src/history/volatileValues.ts"],"sourcesContent":["import { collectPatternsOfType, type FieldSchemaMap } from '../schema/fieldSchema'\nimport type { VolatileMatcher } from './historyCore'\nimport { createPathMatcher } from './pathPatterns'\n\n/**\n * Recognising form-state values that are a field mid-edit rather than data, so\n * the history can decline to record a state it would not be able to restore.\n *\n * Only Payload's JSON field needs this today. Every other field type keeps its\n * editing surface and its form value in the same representation, so whatever\n * the value holds can be dispatched straight back.\n */\n\n/**\n * True for a value Payload's JSON field is holding as raw editor text rather\n * than as parsed data.\n *\n * The field parses on every keystroke and, when the parse throws, writes the\n * editor's text into form state as a string (see @payloadcms/ui fields/JSON,\n * `handleChange`). A string that is not itself valid JSON therefore came from\n * that branch: it is text the editor has not finished, not a value.\n *\n * A JSON field legitimately holding a bare string is indistinguishable from\n * that, since both arrive as a string that does not parse, and Payload keeps no\n * error flag in form state to tell them apart. Reading it as mid-edit is the\n * harmless direction: the path drops out of the history until it holds\n * something else, which is exactly what an ignored path does.\n */\nexport const isUnparsedJson = (value: unknown): boolean => {\n\tif (typeof value !== 'string') return false\n\ttry {\n\t\tJSON.parse(value)\n\t\treturn false\n\t} catch {\n\t\treturn true\n\t}\n}\n\n/**\n * Build the history's volatile matcher from a document's schema.\n *\n * An empty map yields a matcher that is never true, which is also what a\n * document whose schema could not be resolved should get: capturing a value the\n * restore may mangle is worse than the alternative, but guessing which paths\n * are JSON without a schema would be worse still.\n */\nexport const createVolatileMatcher = (map: FieldSchemaMap): VolatileMatcher => {\n\tconst isJsonPath = createPathMatcher(collectPatternsOfType(map, 'json'))\n\treturn (path, field) => isJsonPath(path) && isUnparsedJson(field.value)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,MAAa,kBAAkB,UAA4B;CAC1D,IAAI,OAAO,UAAU,UAAU,OAAO;CACtC,IAAI;EACH,KAAK,MAAM,KAAK;EAChB,OAAO;CACR,QAAQ;EACP,OAAO;CACR;AACD;;;;;;;;;AAUA,MAAa,yBAAyB,QAAyC;CAC9E,MAAM,aAAa,kBAAkB,sBAAsB,KAAK,MAAM,CAAC;CACvE,QAAQ,MAAM,UAAU,WAAW,IAAI,KAAK,eAAe,MAAM,KAAK;AACvE"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ControlsClientProps, DEFAULT_CAPTURE_DEBOUNCE_MS, DEFAULT_SHORTCUTS, DocScope, ResolvedDocOptions, ShortcutKeys, UndoRedoDocOptions, UndoRedoPluginOptions, resolveDocOptions, toClientProps } from "./plugin/options.js";
|
|
2
|
+
import { UNDO_REDO_FIELD_KEY, UndoRedoFieldConfig, undoRedoCustom } from "./schema/fieldConfig.js";
|
|
3
|
+
import { UNDO_REDO_COMPONENT_PATH, undoRedoComponent, withUndoRedo, withUndoRedoGlobal } from "./plugin/withUndoRedo.js";
|
|
4
|
+
|
|
5
|
+
//#region src/index.d.ts
|
|
6
|
+
declare module 'payload' {
|
|
7
|
+
interface RegisteredPlugins {
|
|
8
|
+
'@10x-media/undo-redo': UndoRedoPluginOptions;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Client-side undo/redo for the admin document form, independent of document
|
|
13
|
+
* versions. Adds undo and redo controls before the document controls on every
|
|
14
|
+
* collection and global edit view. History lives in memory for the editor
|
|
15
|
+
* session only; nothing reaches the server until the user saves.
|
|
16
|
+
*/
|
|
17
|
+
declare const undoRedo: (options: UndoRedoPluginOptions) => import("payload").Plugin;
|
|
18
|
+
//#endregion
|
|
19
|
+
export { type ControlsClientProps, DEFAULT_CAPTURE_DEBOUNCE_MS, DEFAULT_SHORTCUTS, type DocScope, type UndoRedoPluginOptions as PluginOptions, type ResolvedDocOptions, type ShortcutKeys, UNDO_REDO_COMPONENT_PATH, UNDO_REDO_FIELD_KEY, type UndoRedoDocOptions, type UndoRedoFieldConfig, type UndoRedoPluginOptions, resolveDocOptions, toClientProps, undoRedo, undoRedoComponent, undoRedoCustom, withUndoRedo, withUndoRedoGlobal };
|
|
20
|
+
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { DEFAULT_CAPTURE_DEBOUNCE_MS, DEFAULT_SHORTCUTS, resolveDocOptions, toClientProps } from "./plugin/options.js";
|
|
2
|
+
import { registerTranslations } from "./plugin/registerTranslations.js";
|
|
3
|
+
import { UNDO_REDO_COMPONENT_PATH, undoRedoComponent, withUndoRedo, withUndoRedoGlobal } from "./plugin/withUndoRedo.js";
|
|
4
|
+
import { UNDO_REDO_FIELD_KEY, undoRedoCustom } from "./schema/fieldConfig.js";
|
|
5
|
+
import { definePlugin } from "payload";
|
|
6
|
+
//#region src/index.ts
|
|
7
|
+
/**
|
|
8
|
+
* Client-side undo/redo for the admin document form, independent of document
|
|
9
|
+
* versions. Adds undo and redo controls before the document controls on every
|
|
10
|
+
* collection and global edit view. History lives in memory for the editor
|
|
11
|
+
* session only; nothing reaches the server until the user saves.
|
|
12
|
+
*/
|
|
13
|
+
const undoRedo = definePlugin({
|
|
14
|
+
slug: "@10x-media/undo-redo",
|
|
15
|
+
plugin: ({ config, plugins: _plugins, ...options }) => {
|
|
16
|
+
if (options.disabled === true) return config;
|
|
17
|
+
registerTranslations(config, options.translations);
|
|
18
|
+
return {
|
|
19
|
+
...config,
|
|
20
|
+
collections: config.collections?.map((each) => withUndoRedo(each, resolveDocOptions(options, "collections", each.slug))),
|
|
21
|
+
globals: config.globals?.map((each) => withUndoRedoGlobal(each, resolveDocOptions(options, "globals", each.slug)))
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
//#endregion
|
|
26
|
+
export { DEFAULT_CAPTURE_DEBOUNCE_MS, DEFAULT_SHORTCUTS, UNDO_REDO_COMPONENT_PATH, UNDO_REDO_FIELD_KEY, resolveDocOptions, toClientProps, undoRedo, undoRedoComponent, undoRedoCustom, withUndoRedo, withUndoRedoGlobal };
|
|
27
|
+
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { type Config, definePlugin } from 'payload'\n\nimport { resolveDocOptions, type UndoRedoPluginOptions } from './plugin/options'\nimport { registerTranslations } from './plugin/registerTranslations'\nimport { withUndoRedo, withUndoRedoGlobal } from './plugin/withUndoRedo'\n\ndeclare module 'payload' {\n\tinterface RegisteredPlugins {\n\t\t'@10x-media/undo-redo': UndoRedoPluginOptions\n\t}\n}\n\n/**\n * Client-side undo/redo for the admin document form, independent of document\n * versions. Adds undo and redo controls before the document controls on every\n * collection and global edit view. History lives in memory for the editor\n * session only; nothing reaches the server until the user saves.\n */\nexport const undoRedo = definePlugin<UndoRedoPluginOptions>({\n\tslug: '@10x-media/undo-redo',\n\tplugin: ({ config, plugins: _plugins, ...options }): Config => {\n\t\tif (options.disabled === true) {\n\t\t\treturn config\n\t\t}\n\t\tregisterTranslations(config, options.translations)\n\t\treturn {\n\t\t\t...config,\n\t\t\tcollections: config.collections?.map((each) =>\n\t\t\t\twithUndoRedo(each, resolveDocOptions(options, 'collections', each.slug))\n\t\t\t),\n\t\t\tglobals: config.globals?.map((each) =>\n\t\t\t\twithUndoRedoGlobal(each, resolveDocOptions(options, 'globals', each.slug))\n\t\t\t),\n\t\t}\n\t},\n})\n\nexport type {\n\tControlsClientProps,\n\tDocScope,\n\tResolvedDocOptions,\n\tShortcutKeys,\n\tUndoRedoDocOptions,\n\tUndoRedoPluginOptions,\n} from './plugin/options'\nexport {\n\tDEFAULT_CAPTURE_DEBOUNCE_MS,\n\tDEFAULT_SHORTCUTS,\n\tresolveDocOptions,\n\ttoClientProps,\n} from './plugin/options'\nexport {\n\tUNDO_REDO_COMPONENT_PATH,\n\tundoRedoComponent,\n\twithUndoRedo,\n\twithUndoRedoGlobal,\n} from './plugin/withUndoRedo'\nexport type { UndoRedoFieldConfig } from './schema/fieldConfig'\nexport { UNDO_REDO_FIELD_KEY, undoRedoCustom } from './schema/fieldConfig'\nexport type { UndoRedoPluginOptions as PluginOptions }\n"],"mappings":";;;;;;;;;;;;AAkBA,MAAa,WAAW,aAAoC;CAC3D,MAAM;CACN,SAAS,EAAE,QAAQ,SAAS,UAAU,GAAG,cAAsB;EAC9D,IAAI,QAAQ,aAAa,MACxB,OAAO;EAER,qBAAqB,QAAQ,QAAQ,YAAY;EACjD,OAAO;GACN,GAAG;GACH,aAAa,OAAO,aAAa,KAAK,SACrC,aAAa,MAAM,kBAAkB,SAAS,eAAe,KAAK,IAAI,CAAC,CACxE;GACA,SAAS,OAAO,SAAS,KAAK,SAC7B,mBAAmB,MAAM,kBAAkB,SAAS,WAAW,KAAK,IAAI,CAAC,CAC1E;EACD;CACD;AACD,CAAC"}
|