@delmaredigital/payload-puck 0.6.30 → 0.7.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/README.md +15 -1
- package/dist/editor/PuckEditorImpl.client.js +36 -3
- package/dist/editor/plugins/VersionHistoryPanel.d.ts +6 -2
- package/dist/editor/plugins/VersionHistoryPanel.js +3 -2
- package/dist/editor/plugins/versionHistoryPlugin.d.ts +4 -3
- package/dist/editor/utils/index.d.ts +1 -0
- package/dist/editor/utils/index.js +1 -0
- package/dist/editor/utils/isDataEqual.d.ts +15 -0
- package/dist/editor/utils/isDataEqual.js +57 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +19 -15
package/README.md
CHANGED
|
@@ -32,7 +32,8 @@ pnpm add @delmaredigital/payload-puck @puckeditor/core
|
|
|
32
32
|
|
|
33
33
|
| Dependency | Version |
|
|
34
34
|
|------------|---------|
|
|
35
|
-
|
|
|
35
|
+
| `node` | >= 20.9.0 |
|
|
36
|
+
| `@puckeditor/core` | >= 0.23.0 |
|
|
36
37
|
| `payload` | >= 3.69.0 |
|
|
37
38
|
| `@payloadcms/next` | >= 3.69.0 |
|
|
38
39
|
| `next` | >= 15.4.8 (see security note below) |
|
|
@@ -42,6 +43,19 @@ pnpm add @delmaredigital/payload-puck @puckeditor/core
|
|
|
42
43
|
|
|
43
44
|
> **Security:** If your app uses Next.js middleware (or proxy.ts) to protect dynamic routes, use `next` >= 15.5.16 / 16.2.5 to pick up the fix for [CVE-2026-44574](https://github.com/vercel/next.js/security/advisories/GHSA-492v-c6pp-mqqv) (middleware bypass via dynamic route parameter injection). Turbopack users need >= 15.5.18 / 16.2.6.
|
|
44
45
|
|
|
46
|
+
### Upgrading to 0.7.0 (breaking)
|
|
47
|
+
|
|
48
|
+
`0.7.0` raises two floors. Both are a one-line change for most projects:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pnpm add @puckeditor/core@^0.23.0 # peer floor moved from >=0.21.0
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
- **`@puckeditor/core` now requires >= 0.23.0.** The Puck plugins this package bundles are versioned in lockstep with Puck core and import it from the host, so running them against an older core is not a supported combination. Puck 0.23 ships a rewritten canvas drag-and-drop engine and a redesigned outline — **the editing experience changes visibly**, even though no API you call has changed. Worth a pass through your editor after upgrading. See the [Puck 0.23 release notes](https://puckeditor.com/blog/puck-023).
|
|
55
|
+
- **Node 18 is no longer supported;** the floor is now `>=20.9.0`. Node 18 is end-of-life and Puck core 0.23 itself requires `>=20.0.0`.
|
|
56
|
+
|
|
57
|
+
No exports, props, or configuration options were removed or renamed. Full detail in the [changelog](./CHANGELOG.md).
|
|
58
|
+
|
|
45
59
|
---
|
|
46
60
|
|
|
47
61
|
## Quick Start
|
|
@@ -16,6 +16,7 @@ import { IframeWrapper } from './components/IframeWrapper.js';
|
|
|
16
16
|
import { PreviewModal } from './components/PreviewModal.js';
|
|
17
17
|
import { DarkModeStyles } from './components/DarkModeStyles.js';
|
|
18
18
|
import { useUnsavedChanges } from './hooks/useUnsavedChanges.js';
|
|
19
|
+
import { isDataEqual } from './utils/isDataEqual.js';
|
|
19
20
|
import { createVersionHistoryPlugin } from './plugins/versionHistoryPlugin.js';
|
|
20
21
|
import { ThemeProvider } from '../theme/index.js';
|
|
21
22
|
import { usePuckConfig } from '../views/PuckConfigContext.js';
|
|
@@ -117,6 +118,11 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
117
118
|
]);
|
|
118
119
|
// Use a ref to track latest data without causing re-renders
|
|
119
120
|
const latestDataRef = useRef(dataWithSlug);
|
|
121
|
+
// Track the last loaded/saved data so we can distinguish real user edits from
|
|
122
|
+
// no-op onChange dispatches (e.g. Puck's mount-time resolve pass). Refreshed at
|
|
123
|
+
// every markClean() site (save / publish) so subsequent edits diff against the
|
|
124
|
+
// most recently persisted state.
|
|
125
|
+
const savedDataRef = useRef(dataWithSlug);
|
|
120
126
|
// Get editor stylesheets from PuckConfigProvider context (as fallback)
|
|
121
127
|
const { editorStylesheets: contextStylesheets, editorCss: contextCss } = usePuckConfig();
|
|
122
128
|
// Props take precedence over context
|
|
@@ -239,6 +245,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
239
245
|
setSaveError(null); // Clear any previous error
|
|
240
246
|
// After saving as draft, update status to draft (shows "Unpublished Changes" if was published)
|
|
241
247
|
setDocumentStatus('draft');
|
|
248
|
+
savedDataRef.current = typedData;
|
|
242
249
|
markClean();
|
|
243
250
|
onSaveSuccess?.(data);
|
|
244
251
|
} catch (error) {
|
|
@@ -287,6 +294,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
287
294
|
setSaveError(null); // Clear any previous error
|
|
288
295
|
setDocumentStatus('published'); // Update status after successful publish
|
|
289
296
|
setWasPublished(true); // Mark as having been published
|
|
297
|
+
savedDataRef.current = typedData;
|
|
290
298
|
markClean();
|
|
291
299
|
onSaveSuccess?.(data);
|
|
292
300
|
} catch (error) {
|
|
@@ -340,9 +348,19 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
340
348
|
// Handle data change
|
|
341
349
|
const handleChange = useCallback((data)=>{
|
|
342
350
|
latestDataRef.current = data;
|
|
343
|
-
|
|
351
|
+
// Only mark dirty when the data actually differs from the last loaded/saved
|
|
352
|
+
// state. Puck fires onChange for no-op mount-time resolves (which can differ
|
|
353
|
+
// from the loaded data only by undefined-valued keys); treating those as
|
|
354
|
+
// edits produces a spurious "Unsaved" flag on load. isDataEqual is
|
|
355
|
+
// undefined-tolerant so those resolves compare equal and clear the flag.
|
|
356
|
+
if (isDataEqual(data, savedDataRef.current)) {
|
|
357
|
+
markClean();
|
|
358
|
+
} else {
|
|
359
|
+
markDirty();
|
|
360
|
+
}
|
|
344
361
|
onChangeProp?.(data);
|
|
345
362
|
}, [
|
|
363
|
+
markClean,
|
|
346
364
|
markDirty,
|
|
347
365
|
onChangeProp
|
|
348
366
|
]);
|
|
@@ -415,6 +433,7 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
415
433
|
setLastSaved(new Date());
|
|
416
434
|
setSaveError(null);
|
|
417
435
|
setDocumentStatus('draft');
|
|
436
|
+
savedDataRef.current = data;
|
|
418
437
|
markClean();
|
|
419
438
|
onSaveSuccess?.(data);
|
|
420
439
|
} catch (error) {
|
|
@@ -505,18 +524,32 @@ import { useAiPrompts } from '../ai/hooks/useAiPrompts.js';
|
|
|
505
524
|
const defaultPlugins = [
|
|
506
525
|
headingAnalyzer
|
|
507
526
|
];
|
|
527
|
+
// Restoring a version has already persisted that data server-side, so it
|
|
528
|
+
// becomes the new saved baseline. Without this, the `setData` dispatch the
|
|
529
|
+
// restore performs fires an onChange that diffs against the pre-restore
|
|
530
|
+
// baseline and immediately re-flags the (already saved) document as dirty.
|
|
531
|
+
const handleRestoreSuccess = useCallback((restoredData)=>{
|
|
532
|
+
if (restoredData) {
|
|
533
|
+
const typedData = restoredData;
|
|
534
|
+
savedDataRef.current = typedData;
|
|
535
|
+
latestDataRef.current = typedData;
|
|
536
|
+
}
|
|
537
|
+
markClean();
|
|
538
|
+
}, [
|
|
539
|
+
markClean
|
|
540
|
+
]);
|
|
508
541
|
// Version history plugin for the plugin rail
|
|
509
542
|
const versionHistoryPlugin = useMemo(()=>{
|
|
510
543
|
if (!pageId) return null;
|
|
511
544
|
return createVersionHistoryPlugin({
|
|
512
545
|
pageId,
|
|
513
546
|
apiEndpoint,
|
|
514
|
-
onRestoreSuccess:
|
|
547
|
+
onRestoreSuccess: handleRestoreSuccess
|
|
515
548
|
});
|
|
516
549
|
}, [
|
|
517
550
|
pageId,
|
|
518
551
|
apiEndpoint,
|
|
519
|
-
|
|
552
|
+
handleRestoreSuccess
|
|
520
553
|
]);
|
|
521
554
|
// Fetch AI prompts client-side when prompts collection is enabled
|
|
522
555
|
// This allows prompts to update in real-time when edited via the prompt editor panel
|
|
@@ -29,9 +29,13 @@ export interface VersionHistoryPanelProps {
|
|
|
29
29
|
*/
|
|
30
30
|
apiEndpoint?: string;
|
|
31
31
|
/**
|
|
32
|
-
* Callback after successful restore (e.g., to mark editor as clean)
|
|
32
|
+
* Callback after successful restore (e.g., to mark editor as clean).
|
|
33
|
+
*
|
|
34
|
+
* Receives the restored data that was dispatched into the editor, so the
|
|
35
|
+
* caller can treat it as the new "last saved" baseline — the restore endpoint
|
|
36
|
+
* has already persisted it server-side.
|
|
33
37
|
*/
|
|
34
|
-
onRestoreSuccess?: () => void;
|
|
38
|
+
onRestoreSuccess?: (restoredData?: Data) => void;
|
|
35
39
|
}
|
|
36
40
|
/**
|
|
37
41
|
* Version history panel for the Puck plugin rail
|
|
@@ -256,8 +256,9 @@ const styles = {
|
|
|
256
256
|
// Show success message
|
|
257
257
|
setSuccessMessage(`Restored version from ${formatDate(version.updatedAt)}`);
|
|
258
258
|
setTimeout(()=>setSuccessMessage(null), 3000);
|
|
259
|
-
// Notify parent to mark as clean
|
|
260
|
-
|
|
259
|
+
// Notify parent to mark as clean, handing over the restored data so it
|
|
260
|
+
// becomes the new saved baseline (the restore already persisted it).
|
|
261
|
+
onRestoreSuccess?.(restoredDoc?.puckData);
|
|
261
262
|
// Refresh version list
|
|
262
263
|
fetchVersions();
|
|
263
264
|
} catch (err) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Plugin } from '@puckeditor/core';
|
|
1
|
+
import type { Data, Plugin } from '@puckeditor/core';
|
|
2
2
|
export interface VersionHistoryPluginOptions {
|
|
3
3
|
/**
|
|
4
4
|
* Page ID to fetch versions for
|
|
@@ -10,9 +10,10 @@ export interface VersionHistoryPluginOptions {
|
|
|
10
10
|
*/
|
|
11
11
|
apiEndpoint?: string;
|
|
12
12
|
/**
|
|
13
|
-
* Callback after successful restore (e.g., to mark editor as clean)
|
|
13
|
+
* Callback after successful restore (e.g., to mark editor as clean).
|
|
14
|
+
* Receives the restored data that was dispatched into the editor.
|
|
14
15
|
*/
|
|
15
|
-
onRestoreSuccess?: () => void;
|
|
16
|
+
onRestoreSuccess?: (restoredData?: Data) => void;
|
|
16
17
|
}
|
|
17
18
|
/**
|
|
18
19
|
* Creates a Puck plugin for version history
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undefined-tolerant deep equality for Puck data.
|
|
3
|
+
*
|
|
4
|
+
* Puck's mount-time resolve pass (`resolveAndCommitData`) can dispatch a `replace`
|
|
5
|
+
* even when the net content is unchanged — e.g. a resolved node differs from the
|
|
6
|
+
* loaded node only by an `undefined`-valued key. Strict deep-equal libraries such
|
|
7
|
+
* as `fast-equals` treat `{}` and `{ k: undefined }` as different, so that no-op
|
|
8
|
+
* resolve looks like a real edit and spuriously marks the document dirty.
|
|
9
|
+
*
|
|
10
|
+
* This comparison treats an absent key and an `undefined`-valued key as equal, so
|
|
11
|
+
* a resolve that only adds/removes `undefined` values compares equal to the loaded
|
|
12
|
+
* data. Object key order is irrelevant (as with any deep equal); array order is
|
|
13
|
+
* significant (Puck content order is meaningful).
|
|
14
|
+
*/
|
|
15
|
+
export declare function isDataEqual(a: unknown, b: unknown): boolean;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undefined-tolerant deep equality for Puck data.
|
|
3
|
+
*
|
|
4
|
+
* Puck's mount-time resolve pass (`resolveAndCommitData`) can dispatch a `replace`
|
|
5
|
+
* even when the net content is unchanged — e.g. a resolved node differs from the
|
|
6
|
+
* loaded node only by an `undefined`-valued key. Strict deep-equal libraries such
|
|
7
|
+
* as `fast-equals` treat `{}` and `{ k: undefined }` as different, so that no-op
|
|
8
|
+
* resolve looks like a real edit and spuriously marks the document dirty.
|
|
9
|
+
*
|
|
10
|
+
* This comparison treats an absent key and an `undefined`-valued key as equal, so
|
|
11
|
+
* a resolve that only adds/removes `undefined` values compares equal to the loaded
|
|
12
|
+
* data. Object key order is irrelevant (as with any deep equal); array order is
|
|
13
|
+
* significant (Puck content order is meaningful).
|
|
14
|
+
*/ export function isDataEqual(a, b) {
|
|
15
|
+
if (Object.is(a, b)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
// NaN is handled by Object.is above; remaining primitives that differ are unequal.
|
|
19
|
+
if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
const aIsArray = Array.isArray(a);
|
|
23
|
+
const bIsArray = Array.isArray(b);
|
|
24
|
+
if (aIsArray !== bIsArray) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (aIsArray && bIsArray) {
|
|
28
|
+
if (a.length !== b.length) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
for(let i = 0; i < a.length; i++){
|
|
32
|
+
if (!isDataEqual(a[i], b[i])) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
const aObj = a;
|
|
39
|
+
const bObj = b;
|
|
40
|
+
// Compare the union of keys, treating a missing key and an `undefined` value as
|
|
41
|
+
// equivalent so that `{}` and `{ k: undefined }` are considered equal.
|
|
42
|
+
const keys = new Set([
|
|
43
|
+
...Object.keys(aObj),
|
|
44
|
+
...Object.keys(bObj)
|
|
45
|
+
]);
|
|
46
|
+
for (const key of keys){
|
|
47
|
+
const aVal = aObj[key];
|
|
48
|
+
const bVal = bObj[key];
|
|
49
|
+
if (aVal === undefined && bVal === undefined) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!isDataEqual(aVal, bVal)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.7.0";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Auto-generated by scripts/generate-version.js - do not edit manually
|
|
2
|
-
export const VERSION = '0.
|
|
2
|
+
export const VERSION = '0.7.0';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@delmaredigital/payload-puck",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Puck visual page builder plugin for Payload CMS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -104,20 +104,22 @@
|
|
|
104
104
|
"scripts": {
|
|
105
105
|
"dev-publish": "pnpm build && pnpm version prerelease --preid=dev --no-git-tag-version && pnpm publish --registry http://localhost:4873 --no-git-checks --tag dev",
|
|
106
106
|
"generate-version": "node scripts/generate-version.js",
|
|
107
|
-
"
|
|
108
|
-
"build": "pnpm build:swc && pnpm build:types && pnpm copyfiles",
|
|
107
|
+
"build": "pnpm generate-version && pnpm build:swc && pnpm build:types && pnpm copyfiles",
|
|
109
108
|
"build:swc": "swc ./src -d ./dist --strip-leading-paths",
|
|
110
109
|
"build:types": "tsc --outDir dist",
|
|
111
110
|
"copyfiles": "copyfiles -u 1 \"src/**/*.css\" dist",
|
|
112
111
|
"dev": "pnpm generate-version && swc ./src -d ./dist --strip-leading-paths --watch",
|
|
113
|
-
"typecheck": "tsc --noEmit",
|
|
112
|
+
"typecheck": "pnpm generate-version && tsc --noEmit && tsc -p tsconfig.test.json",
|
|
113
|
+
"test": "pnpm generate-version && vitest run",
|
|
114
|
+
"test:watch": "pnpm generate-version && vitest",
|
|
115
|
+
"test:coverage": "pnpm generate-version && vitest run --coverage",
|
|
114
116
|
"clean": "rm -rf dist",
|
|
115
117
|
"prepublishOnly": "pnpm build"
|
|
116
118
|
},
|
|
117
119
|
"peerDependencies": {
|
|
118
120
|
"@payloadcms/next": ">=3.69.0",
|
|
119
121
|
"@payloadcms/ui": ">=3.69.0",
|
|
120
|
-
"@puckeditor/core": ">=0.
|
|
122
|
+
"@puckeditor/core": ">=0.23.0",
|
|
121
123
|
"@tailwindcss/postcss": ">=4.0.0",
|
|
122
124
|
"next": ">=15.4.8",
|
|
123
125
|
"payload": ">=3.69.0",
|
|
@@ -152,9 +154,9 @@
|
|
|
152
154
|
}
|
|
153
155
|
},
|
|
154
156
|
"dependencies": {
|
|
155
|
-
"@puckeditor/cloud-client": "^0.
|
|
156
|
-
"@puckeditor/plugin-ai": "^0.
|
|
157
|
-
"@puckeditor/plugin-heading-analyzer": "^0.
|
|
157
|
+
"@puckeditor/cloud-client": "^0.8.2",
|
|
158
|
+
"@puckeditor/plugin-ai": "^0.8.2",
|
|
159
|
+
"@puckeditor/plugin-heading-analyzer": "^0.23.0",
|
|
158
160
|
"@radix-ui/react-popover": "^1.1.15",
|
|
159
161
|
"@tiptap/core": "^3.20.1",
|
|
160
162
|
"@tiptap/extension-color": "^3.20.1",
|
|
@@ -171,20 +173,22 @@
|
|
|
171
173
|
"lucide-react": "^0.469.0"
|
|
172
174
|
},
|
|
173
175
|
"devDependencies": {
|
|
174
|
-
"@payloadcms/next": "^3.
|
|
175
|
-
"@payloadcms/ui": "^3.
|
|
176
|
-
"@puckeditor/core": "^0.
|
|
176
|
+
"@payloadcms/next": "^3.87.1",
|
|
177
|
+
"@payloadcms/ui": "^3.87.1",
|
|
178
|
+
"@puckeditor/core": "^0.23.0",
|
|
177
179
|
"@swc/cli": "^0.6.0",
|
|
178
180
|
"@swc/core": "^1.15.18",
|
|
179
181
|
"@types/node": "^24.12.0",
|
|
180
182
|
"@types/react": "^19.2.14",
|
|
181
183
|
"@types/react-dom": "^19.2.3",
|
|
184
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
182
185
|
"copyfiles": "^2.4.1",
|
|
183
|
-
"next": "^16.
|
|
184
|
-
"payload": "^3.
|
|
186
|
+
"next": "^16.3.0",
|
|
187
|
+
"payload": "^3.87.1",
|
|
185
188
|
"react": "^19.2.4",
|
|
186
189
|
"react-dom": "^19.2.4",
|
|
187
|
-
"typescript": "^5.9.3"
|
|
190
|
+
"typescript": "^5.9.3",
|
|
191
|
+
"vitest": "^3.2.7"
|
|
188
192
|
},
|
|
189
193
|
"keywords": [
|
|
190
194
|
"payload",
|
|
@@ -196,7 +200,7 @@
|
|
|
196
200
|
"cms"
|
|
197
201
|
],
|
|
198
202
|
"engines": {
|
|
199
|
-
"node": "
|
|
203
|
+
"node": ">=20.9.0"
|
|
200
204
|
},
|
|
201
205
|
"author": "Delmare Digital",
|
|
202
206
|
"repository": {
|