payload-nested-docs-page-tree 1.4.0 → 1.4.1
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/LICENSE +21 -21
- package/README.md +276 -231
- package/dist/components/PageTreeContext.js.map +1 -1
- package/dist/components/PageTreeListView.client.js.map +1 -1
- package/dist/components/PageTreeListView.module.css +358 -358
- package/dist/components/PageTreeListView.server.js.map +1 -1
- package/dist/components/PageTreeTitleCell.js.map +1 -1
- package/dist/endpoints/createMovePageEndpoint.d.ts +1 -0
- package/dist/endpoints/createMovePageEndpoint.js +24 -6
- package/dist/endpoints/createMovePageEndpoint.js.map +1 -1
- package/dist/endpoints/createReorderPageEndpoint.js +4 -1
- package/dist/endpoints/createReorderPageEndpoint.js.map +1 -1
- package/dist/exports/client.js.map +1 -1
- package/dist/exports/rsc.js.map +1 -1
- package/dist/index.js +11 -7
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +38 -0
- package/dist/types.js +22 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/badgeConfig.js.map +1 -1
- package/dist/utilities/dropTargets.js.map +1 -1
- package/dist/utilities/moveValidation.js.map +1 -1
- package/dist/utilities/pageTree.js.map +1 -1
- package/dist/utilities/pageTreeCollision.js.map +1 -1
- package/dist/utilities/pageTreeConfig.js.map +1 -1
- package/dist/utilities/status.js.map +1 -1
- package/package.json +42 -21
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 WLF Studios
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 WLF Studios
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,54 +1,54 @@
|
|
|
1
|
-
# payload-nested-docs-page-tree
|
|
2
|
-
|
|
3
|
-
Companion admin plugin for [`@payloadcms/plugin-nested-docs`](https://payloadcms.com/docs/plugins/nested-docs).
|
|
4
|
-
|
|
1
|
+
# payload-nested-docs-page-tree
|
|
2
|
+
|
|
3
|
+
Companion admin plugin for [`@payloadcms/plugin-nested-docs`](https://payloadcms.com/docs/plugins/nested-docs).
|
|
4
|
+
|
|
5
5
|
<p align="center">
|
|
6
6
|
<img alt="Page tree admin overview" src="assets/page-tree-admin-overview.png" width="100%" />
|
|
7
7
|
</p>
|
|
8
|
-
|
|
8
|
+
|
|
9
9
|
Adds a nested tree list view for nested docs collections in Payload admin, with visual hierarchy shading, live URL path previews, separate reorder and parent-move interactions, and status badges for published / changed / draft documents.
|
|
10
|
-
|
|
11
|
-
It works alongside `@payloadcms/plugin-nested-docs`. It does not replace nested docs persistence, breadcrumbs generation, or routing.
|
|
12
|
-
|
|
13
|
-
Tested with Payload `3.81` and Next.js `16.2`.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
## Install
|
|
17
|
-
|
|
18
|
-
```bash
|
|
19
|
-
pnpm add payload-nested-docs-page-tree
|
|
20
|
-
```
|
|
21
|
-
|
|
22
|
-
## Quick Setup
|
|
23
|
-
|
|
24
|
-
`@payloadcms/plugin-nested-docs` should already be installed, and each target collection should already have:
|
|
25
|
-
|
|
26
|
-
- a nested docs parent field
|
|
27
|
-
- a nested docs breadcrumbs field
|
|
28
|
-
- a top-level `admin.useAsTitle` field
|
|
29
|
-
|
|
30
|
-
Add `nestedDocsPageTreePlugin(...)` right after `nestedDocsPlugin(...)`:
|
|
31
|
-
|
|
32
|
-
```ts
|
|
33
|
-
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
|
34
|
-
import { nestedDocsPageTreePlugin } from 'payload-nested-docs-page-tree'
|
|
35
|
-
|
|
36
|
-
export const plugins = [
|
|
37
|
-
nestedDocsPlugin({
|
|
38
|
-
// your existing nested docs config
|
|
39
|
-
}),
|
|
40
|
-
nestedDocsPageTreePlugin({
|
|
41
|
-
collections: ['pages'],
|
|
42
|
-
}),
|
|
43
|
-
]
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Refresh the admin import map:
|
|
47
|
-
|
|
48
|
-
```bash
|
|
49
|
-
payload generate:importmap
|
|
50
|
-
```
|
|
51
|
-
|
|
10
|
+
|
|
11
|
+
It works alongside `@payloadcms/plugin-nested-docs`. It does not replace nested docs persistence, breadcrumbs generation, or routing.
|
|
12
|
+
|
|
13
|
+
Tested with Payload `3.81` and Next.js `16.2`.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add payload-nested-docs-page-tree
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Setup
|
|
23
|
+
|
|
24
|
+
`@payloadcms/plugin-nested-docs` should already be installed, and each target collection should already have:
|
|
25
|
+
|
|
26
|
+
- a nested docs parent field
|
|
27
|
+
- a nested docs breadcrumbs field
|
|
28
|
+
- a top-level `admin.useAsTitle` field
|
|
29
|
+
|
|
30
|
+
Add `nestedDocsPageTreePlugin(...)` right after `nestedDocsPlugin(...)`:
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
|
34
|
+
import { nestedDocsPageTreePlugin } from 'payload-nested-docs-page-tree'
|
|
35
|
+
|
|
36
|
+
export const plugins = [
|
|
37
|
+
nestedDocsPlugin({
|
|
38
|
+
// your existing nested docs config
|
|
39
|
+
}),
|
|
40
|
+
nestedDocsPageTreePlugin({
|
|
41
|
+
collections: ['pages'],
|
|
42
|
+
}),
|
|
43
|
+
]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Refresh the admin import map:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
payload generate:importmap
|
|
50
|
+
```
|
|
51
|
+
|
|
52
52
|
## What It Adds
|
|
53
53
|
|
|
54
54
|
- replaces the collection list view with a nested tree table
|
|
@@ -87,58 +87,90 @@ Parent moves and orderable reordering are separate interactions.
|
|
|
87
87
|
|
|
88
88
|
For collections using Payload `orderable`, the reorder handle is shown only when the current sort is the orderable field. Reordering only changes the order key and is limited to documents under the same parent, or documents at the root level. A reorder drag cannot change a document's parent.
|
|
89
89
|
|
|
90
|
-
|
|
90
|
+
https://github.com/user-attachments/assets/b25ffa1a-a6bd-45cf-bce8-56ba6cdf7e72
|
|
91
|
+
|
|
92
|
+
|
|
91
93
|
|
|
92
94
|
Parent moves are hidden by default because they change the page hierarchy. Editors can enable the parent-move handle with the `Edit Hierarchy` button when they intentionally want to move pages between parents. Drag a page onto another page to make it a child of that page. Drag it between root pages to move it back to the root level.
|
|
93
95
|
|
|
94
|
-
|
|
96
|
+
Edit hierarchy mode
|
|
97
|
+
|
|
98
|
+
https://github.com/user-attachments/assets/618d5e53-5918-40be-9932-0f516e5e82ba
|
|
99
|
+
|
|
100
|
+
Parent move to child
|
|
101
|
+
|
|
102
|
+
https://github.com/user-attachments/assets/4cb25109-e515-4955-8503-39fddac0020f
|
|
103
|
+
|
|
104
|
+
Parent move back to root
|
|
105
|
+
|
|
106
|
+
https://github.com/user-attachments/assets/470cb5b3-61c8-4b5b-a6e4-5e855f58a0e4
|
|
107
|
+
|
|
108
|
+
Same-parent reorder guard
|
|
109
|
+
|
|
110
|
+
https://github.com/user-attachments/assets/2513ff04-192e-4fdf-808f-6004f55d871c
|
|
95
111
|
|
|
96
|
-
[Watch the parent move to child demo](assets/parent-move-to-child.mp4)
|
|
97
112
|
|
|
98
|
-
[Watch the parent move back to root demo](assets/parent-move-to-root.mp4)
|
|
99
113
|
|
|
100
|
-
[Watch the same-parent reorder guard demo](assets/reorder-same-parent-only.mp4)
|
|
101
114
|
|
|
102
115
|
Internally, parent moves call this plugin's `/:id/move` endpoint. That endpoint updates the nested docs parent field through Payload's local API, while `@payloadcms/plugin-nested-docs` continues to own parent and breadcrumb behavior through its normal fields and hooks.
|
|
103
116
|
|
|
117
|
+
### Publishing moves
|
|
118
|
+
|
|
119
|
+
On a drafts-enabled collection a move is written through the drafts system, so by default it is staged as a **draft**: the tree updates immediately and the page shows a `changed` badge, but the live URL/path changes only when the page is next published. This is the safe default — publishing a move for a page that also has other pending edits would publish those edits too.
|
|
120
|
+
|
|
121
|
+
Set `publishOnMove: true` to publish a move as soon as it happens — but only when it is safe to:
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
nestedDocsPageTreePlugin({
|
|
125
|
+
collections: ['pages'],
|
|
126
|
+
publishOnMove: true,
|
|
127
|
+
})
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
With `publishOnMove` enabled, a move is published immediately **only if the moved page had no unpublished changes beforehand** (its latest version was already published). If the page has pending draft edits (a `changed` or draft-only page), the move stays staged exactly as before, so in-progress edits are never published as a side effect of a move. Collections without drafts always move live and are unaffected by this option.
|
|
131
|
+
|
|
132
|
+
Publishing a move also republishes the moved page's descendants, so their live URLs follow the new parent. Since the live site really did change, a published move **does** trigger your deploy hook, once for the whole subtree - see [Deploy Hooks](#the-fix). Reorders and staged moves still do not. You do not need to change your hook when enabling this option.
|
|
133
|
+
|
|
134
|
+
On a localized collection the parent field is shared across locales but breadcrumbs are not: publishing a move recomputes breadcrumbs only for the locale the move was made in, so other locales keep their previous URL until they are next published. If that matters for your setup, leave `publishOnMove` off.
|
|
135
|
+
|
|
104
136
|
## Home Indicator
|
|
105
137
|
|
|
106
138
|
By default, the home icon is enabled only for the `pages` collection.
|
|
107
|
-
|
|
108
|
-
```ts
|
|
109
|
-
nestedDocsPageTreePlugin({
|
|
110
|
-
collections: ['pages'],
|
|
111
|
-
})
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
For custom page collection slugs, pass an exact allow-list:
|
|
115
|
-
|
|
116
|
-
```ts
|
|
117
|
-
nestedDocsPageTreePlugin({
|
|
118
|
-
collections: ['page-tree', 'categories'],
|
|
119
|
-
homeIndicator: {
|
|
120
|
-
collections: ['page-tree'],
|
|
121
|
-
},
|
|
122
|
-
})
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
To disable the home icon everywhere:
|
|
126
|
-
|
|
127
|
-
```ts
|
|
128
|
-
nestedDocsPageTreePlugin({
|
|
129
|
-
collections: ['pages'],
|
|
130
|
-
homeIndicator: false,
|
|
131
|
-
})
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
## Status Badges
|
|
135
|
-
|
|
136
|
-
The tree view supports three document states:
|
|
137
|
-
|
|
138
|
-
- `published`: live and up to date
|
|
139
|
-
- `changed`: live, but has unpublished changes
|
|
140
|
-
- `draft`: not published
|
|
141
|
-
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
nestedDocsPageTreePlugin({
|
|
142
|
+
collections: ['pages'],
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
For custom page collection slugs, pass an exact allow-list:
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
nestedDocsPageTreePlugin({
|
|
150
|
+
collections: ['page-tree', 'categories'],
|
|
151
|
+
homeIndicator: {
|
|
152
|
+
collections: ['page-tree'],
|
|
153
|
+
},
|
|
154
|
+
})
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
To disable the home icon everywhere:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
nestedDocsPageTreePlugin({
|
|
161
|
+
collections: ['pages'],
|
|
162
|
+
homeIndicator: false,
|
|
163
|
+
})
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Status Badges
|
|
167
|
+
|
|
168
|
+
The tree view supports three document states:
|
|
169
|
+
|
|
170
|
+
- `published`: live and up to date
|
|
171
|
+
- `changed`: live, but has unpublished changes
|
|
172
|
+
- `draft`: not published
|
|
173
|
+
|
|
142
174
|
Badge colors use Payload theme colors by default. To override badge labels or status colors, pass a `badges` object. Custom colors are treated as one base color per status and are adapted for both light and dark Payload themes:
|
|
143
175
|
|
|
144
176
|
<p align="center">
|
|
@@ -147,40 +179,41 @@ Badge colors use Payload theme colors by default. To override badge labels or st
|
|
|
147
179
|
|
|
148
180
|
```ts
|
|
149
181
|
nestedDocsPageTreePlugin({
|
|
150
|
-
collections: ['pages'],
|
|
151
|
-
badges: {
|
|
152
|
-
colors: {
|
|
153
|
-
// Example: use a custom green / blue / orange palette.
|
|
154
|
-
published: '#bbf3b0',
|
|
155
|
-
changed: '#b9eaf3',
|
|
156
|
-
draft: '#f8d5a7',
|
|
182
|
+
collections: ['pages'],
|
|
183
|
+
badges: {
|
|
184
|
+
colors: {
|
|
185
|
+
// Example: use a custom green / blue / orange palette.
|
|
186
|
+
published: '#bbf3b0',
|
|
187
|
+
changed: '#b9eaf3',
|
|
188
|
+
draft: '#f8d5a7',
|
|
157
189
|
},
|
|
158
190
|
labels: {
|
|
159
191
|
// Example: use custom labels for states.
|
|
160
192
|
published: 'Live',
|
|
161
193
|
changed: 'Has Changes',
|
|
162
194
|
draft: 'Draft Only',
|
|
163
|
-
},
|
|
164
|
-
},
|
|
165
|
-
}),
|
|
166
|
-
```
|
|
167
|
-
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
}),
|
|
198
|
+
```
|
|
199
|
+
|
|
168
200
|
`labels` and `colors` are optional partial overrides. Missing entries fall back to the built-in Payload-themed defaults for published, changed, and draft states.
|
|
169
|
-
|
|
170
|
-
## Configuration
|
|
171
|
-
|
|
172
|
-
- `collections`: target collection slugs
|
|
173
|
-
- `parentFieldSlug`: defaults to `'parent'`
|
|
174
|
-
- `breadcrumbsFieldSlug`: defaults to `'breadcrumbs'`
|
|
175
|
-
- `defaultLimit`: defaults to `100`
|
|
176
|
-
- `hideBreadcrumbs`: defaults to `true`
|
|
177
|
-
- `disabled`: defaults to `false`
|
|
178
|
-
- `homeIndicator`: defaults to `{ collections: ['pages'] }`; set to `false` to disable
|
|
179
|
-
- `
|
|
180
|
-
- `
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
201
|
+
|
|
202
|
+
## Configuration
|
|
203
|
+
|
|
204
|
+
- `collections`: target collection slugs
|
|
205
|
+
- `parentFieldSlug`: defaults to `'parent'`
|
|
206
|
+
- `breadcrumbsFieldSlug`: defaults to `'breadcrumbs'`
|
|
207
|
+
- `defaultLimit`: defaults to `100`
|
|
208
|
+
- `hideBreadcrumbs`: defaults to `true`
|
|
209
|
+
- `disabled`: defaults to `false`
|
|
210
|
+
- `homeIndicator`: defaults to `{ collections: ['pages'] }`; set to `false` to disable
|
|
211
|
+
- `publishOnMove`: defaults to `false`. When `true`, a move is published immediately if the moved page had no unpublished changes; pages with pending edits stay staged. See [Publishing moves](#publishing-moves).
|
|
212
|
+
- `badges`: optional label and color overrides for `published`, `changed`, and `draft`
|
|
213
|
+
- `diagnostics`: defaults to `false`. Enables structured diagnostic logging for tree-related publish/draft regressions; see below.
|
|
214
|
+
|
|
215
|
+
## Diagnostics Mode
|
|
216
|
+
|
|
184
217
|
If a page-tree move or reorder is doing something unexpected, enable diagnostics and reproduce. Each page-tree-triggered write emits one or more structured events on the dev server stdout:
|
|
185
218
|
|
|
186
219
|
<p align="center">
|
|
@@ -189,123 +222,135 @@ If a page-tree move or reorder is doing something unexpected, enable diagnostics
|
|
|
189
222
|
|
|
190
223
|
```ts
|
|
191
224
|
nestedDocsPageTreePlugin({
|
|
192
|
-
collections: ['pages'],
|
|
193
|
-
diagnostics: true,
|
|
194
|
-
})
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
Or with a custom sink:
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
nestedDocsPageTreePlugin({
|
|
201
|
-
collections: ['pages'],
|
|
202
|
-
diagnostics: {
|
|
203
|
-
enabled: true,
|
|
204
|
-
logger: (event) => req.payload.logger.info({ pageTree: event }),
|
|
205
|
-
},
|
|
206
|
-
})
|
|
207
|
-
```
|
|
208
|
-
|
|
225
|
+
collections: ['pages'],
|
|
226
|
+
diagnostics: true,
|
|
227
|
+
})
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Or with a custom sink:
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
nestedDocsPageTreePlugin({
|
|
234
|
+
collections: ['pages'],
|
|
235
|
+
diagnostics: {
|
|
236
|
+
enabled: true,
|
|
237
|
+
logger: (event) => req.payload.logger.info({ pageTree: event }),
|
|
238
|
+
},
|
|
239
|
+
})
|
|
240
|
+
```
|
|
241
|
+
|
|
209
242
|
Each event is one line tagged `[payload-nested-docs-page-tree]` followed by the event source (`move-endpoint:enter`, `move-endpoint:ok`, `move-endpoint:error`, `reorder-endpoint:enter`, `reorder-endpoint:ok`, `reorder-endpoint:error`, `page-tree-change:after`, `page-tree-change:status-flip`) and a JSON payload that includes:
|
|
210
|
-
|
|
243
|
+
|
|
211
244
|
- `flow`: id shared by every event for one logical operation
|
|
212
|
-
- `publishedMainRowBefore` / `publishedMainRowAfter`: fresh reads of the public/published row (`draft: false`)
|
|
213
|
-
- `before` / `after` / `changed`: projected diffs for `_status`, the parent field, and the orderable field when present
|
|
214
|
-
|
|
215
|
-
If the published main row goes from `published` to anything else as a result of a page-tree change, the plugin additionally emits a `page-tree-change:status-flip` WARN line.
|
|
216
|
-
|
|
245
|
+
- `publishedMainRowBefore` / `publishedMainRowAfter`: fresh reads of the public/published row (`draft: false`)
|
|
246
|
+
- `before` / `after` / `changed`: projected diffs for `_status`, the parent field, and the orderable field when present
|
|
247
|
+
|
|
248
|
+
If the published main row goes from `published` to anything else as a result of a page-tree change, the plugin additionally emits a `page-tree-change:status-flip` WARN line.
|
|
249
|
+
|
|
217
250
|
Diagnostics is opt-in and adds extra reads per operation. Leave it off in production unless you are actively investigating.
|
|
218
|
-
|
|
219
|
-
## Drag-And-Drop Is Triggering A Deploy?
|
|
220
|
-
|
|
221
|
-
A drag-and-drop parent move calls `payload.update()` on the draft only. The published version of the live site is never touched. So in most setups, dragging a page does not trigger any rebuild and you can skip this section.
|
|
222
|
-
|
|
223
|
-
### When you can skip this section
|
|
224
|
-
|
|
225
|
-
- The default Payload website template on Vercel (or any host using Next.js ISR), with drafts and autosave on. The template's `afterChange` hook only calls `revalidatePath` and `revalidateTag` from `next/cache`. Those just clear the edge cache. They do not trigger a Vercel build, do not consume build minutes, and do not change what visitors see when the published HTML has not changed.
|
|
226
|
-
- Any setup where your `afterChange` hooks only do in-process cache work (`revalidatePath`, `revalidateTag`, in-memory caches, etc.).
|
|
227
|
-
|
|
228
|
-
### When you need the one-line fix
|
|
229
|
-
|
|
230
|
-
You need the fix if **you** wrote an `afterChange` hook that calls something external or expensive on every save. Common cases:
|
|
231
|
-
|
|
232
|
-
- **Cloudflare Pages / Netlify / Vercel Deploy Hooks** (`fetch(DEPLOY_HOOK_URL)`) - these trigger full rebuilds and burn build minutes.
|
|
233
|
-
- **GitHub Actions** `repository_dispatch` triggers.
|
|
234
|
-
- **Manually-invoked SSG rebuilds**.
|
|
235
|
-
- **Publish notifications** (email, Slack) on status transitions.
|
|
236
|
-
- **Heavy search reindex jobs** (Algolia, Meilisearch full-document push).
|
|
237
|
-
|
|
238
|
-
Why a tree move trips these: a typical deploy hook fires when `previousDoc?._status === 'published'` so that it catches unpublish events too. A tree move on a published doc can match that condition, but the live site has not actually changed. Without the fix, every drag can fire your deploy.
|
|
239
|
-
|
|
240
|
-
### The fix
|
|
241
|
-
|
|
242
|
-
Add one line at the top of your hook. The plugin sets a flag on Payload's [hook context](https://payloadcms.com/docs/hooks/context) for every page-tree
|
|
243
|
-
|
|
244
|
-
```ts
|
|
245
|
-
import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
|
|
246
|
-
|
|
247
|
-
// at the top of your afterChange hook:
|
|
248
|
-
if (req.context?.[pageTreeMoveContextKey]) return
|
|
249
|
-
```
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
pnpm
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
251
|
+
|
|
252
|
+
## Drag-And-Drop Is Triggering A Deploy?
|
|
253
|
+
|
|
254
|
+
A drag-and-drop parent move calls `payload.update()` on the draft only. The published version of the live site is never touched. So in most setups, dragging a page does not trigger any rebuild and you can skip this section.
|
|
255
|
+
|
|
256
|
+
### When you can skip this section
|
|
257
|
+
|
|
258
|
+
- The default Payload website template on Vercel (or any host using Next.js ISR), with drafts and autosave on. The template's `afterChange` hook only calls `revalidatePath` and `revalidateTag` from `next/cache`. Those just clear the edge cache. They do not trigger a Vercel build, do not consume build minutes, and do not change what visitors see when the published HTML has not changed.
|
|
259
|
+
- Any setup where your `afterChange` hooks only do in-process cache work (`revalidatePath`, `revalidateTag`, in-memory caches, etc.).
|
|
260
|
+
|
|
261
|
+
### When you need the one-line fix
|
|
262
|
+
|
|
263
|
+
You need the fix if **you** wrote an `afterChange` hook that calls something external or expensive on every save. Common cases:
|
|
264
|
+
|
|
265
|
+
- **Cloudflare Pages / Netlify / Vercel Deploy Hooks** (`fetch(DEPLOY_HOOK_URL)`) - these trigger full rebuilds and burn build minutes.
|
|
266
|
+
- **GitHub Actions** `repository_dispatch` triggers.
|
|
267
|
+
- **Manually-invoked SSG rebuilds**.
|
|
268
|
+
- **Publish notifications** (email, Slack) on status transitions.
|
|
269
|
+
- **Heavy search reindex jobs** (Algolia, Meilisearch full-document push).
|
|
270
|
+
|
|
271
|
+
Why a tree move trips these: a typical deploy hook fires when `previousDoc?._status === 'published'` so that it catches unpublish events too. A tree move on a published doc can match that condition, but the live site has not actually changed. Without the fix, every drag can fire your deploy.
|
|
272
|
+
|
|
273
|
+
### The fix
|
|
274
|
+
|
|
275
|
+
Add one line at the top of your hook. The plugin sets a flag on Payload's [hook context](https://payloadcms.com/docs/hooks/context) for every page-tree write that leaves the live site unchanged, and your hook reads it to bail out early:
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
|
|
279
|
+
|
|
280
|
+
// at the top of your afterChange hook:
|
|
281
|
+
if (req.context?.[pageTreeMoveContextKey]) return
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
The rule is simply **rebuild when the live site changed**, so this one line stays correct in every configuration:
|
|
285
|
+
|
|
286
|
+
| What the editor did in the tree | Live site changed? | Your deploy hook |
|
|
287
|
+
| --- | --- | --- |
|
|
288
|
+
| Reordered siblings | no - only the order key was written | does not fire |
|
|
289
|
+
| Moved a page, default settings | no - the move is staged as a draft | does not fire |
|
|
290
|
+
| Moved a page with [`publishOnMove`](#publishing-moves) on | yes - the page and its descendants have new URLs | fires once |
|
|
291
|
+
|
|
292
|
+
Only the last row publishes anything, which is why it is the only row that rebuilds. It fires once for the whole subtree, not once per descendant. Enabling `publishOnMove` needs no change to your hook - the same line already does the right thing.
|
|
293
|
+
|
|
294
|
+
To rebuild after **every** page-tree drag, including reorders, just leave the guard out. That also rebuilds on staged moves, which change nothing live.
|
|
295
|
+
|
|
296
|
+
This goes in **your** hook - the one that calls the deploy webhook. Not in any of the template's stock files.
|
|
297
|
+
|
|
298
|
+
Full example:
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
import type { CollectionAfterChangeHook } from 'payload'
|
|
302
|
+
|
|
303
|
+
import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
|
|
304
|
+
|
|
305
|
+
export const triggerDeployOnPublishedChange: CollectionAfterChangeHook = async ({
|
|
306
|
+
doc,
|
|
307
|
+
previousDoc,
|
|
308
|
+
req,
|
|
309
|
+
}) => {
|
|
310
|
+
// -- plugin opt-out --
|
|
311
|
+
if (req.context?.[pageTreeMoveContextKey]) return
|
|
312
|
+
|
|
313
|
+
// -- your deploy logic (example) --
|
|
314
|
+
// Fire on publish, republish, or unpublish - every transition the live site cares about.
|
|
315
|
+
if (doc._status === 'published' || previousDoc?._status === 'published') {
|
|
316
|
+
// POST to your Cloudflare / Netlify / Vercel deploy hook here
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
See `dev/lib/rebuild.ts` for the full Cloudflare deploy hook example used by the dev playground.
|
|
322
|
+
|
|
323
|
+
## Development
|
|
324
|
+
|
|
325
|
+
For local plugin development, use the internal `dev/` app:
|
|
326
|
+
|
|
327
|
+
```bash
|
|
328
|
+
pnpm install
|
|
329
|
+
pnpm dev
|
|
330
|
+
pnpm generate:types
|
|
331
|
+
pnpm generate:importmap
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
Plugin source is in `src/`. The internal test app is in `dev/`.
|
|
335
|
+
|
|
336
|
+
For checks:
|
|
337
|
+
|
|
338
|
+
```bash
|
|
339
|
+
pnpm test:int
|
|
340
|
+
pnpm exec tsc --noEmit
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Test in Another Project
|
|
344
|
+
|
|
345
|
+
For release validation, test the packed artifact instead of a live source-folder dependency:
|
|
346
|
+
|
|
347
|
+
```bash
|
|
348
|
+
pnpm build
|
|
349
|
+
pnpm pack
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Then in the external consumer app:
|
|
353
|
+
|
|
354
|
+
```bash
|
|
355
|
+
pnpm add /path/payload-nested-docs-page-tree-*.tgz
|
|
356
|
+
```
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/components/PageTreeContext.tsx"],"sourcesContent":["'use client'\
|
|
1
|
+
{"version":3,"sources":["../../src/components/PageTreeContext.tsx"],"sourcesContent":["'use client'\n\nimport type { DraggableAttributes, DraggableSyntheticListeners } from '@dnd-kit/core'\n\nimport React from 'react'\n\ntype PageTreeContextValue = {\n activeDragRowID: null | string\n canMoveDocs: boolean\n collapsedIDs: ReadonlySet<string>\n parentMoveEnabled: boolean\n pendingMoveRowID: null | string\n toggleRow: (rowID: string) => void\n}\n\ntype PageTreeRowDndContextValue = {\n isOrderDragging: boolean\n orderHandleAttributes: DraggableAttributes\n orderHandleListeners: DraggableSyntheticListeners\n orderHandleRef: (element: HTMLElement | null) => void\n}\n\nconst PageTreeContext = React.createContext<null | PageTreeContextValue>(null)\nconst PageTreeRowDndContext = React.createContext<null | PageTreeRowDndContextValue>(null)\n\nexport function PageTreeProvider({\n children,\n value,\n}: {\n children: React.ReactNode\n value: PageTreeContextValue\n}) {\n return <PageTreeContext.Provider value={value}>{children}</PageTreeContext.Provider>\n}\n\nexport function PageTreeRowDndProvider({\n children,\n value,\n}: {\n children: React.ReactNode\n value: PageTreeRowDndContextValue\n}) {\n return <PageTreeRowDndContext.Provider value={value}>{children}</PageTreeRowDndContext.Provider>\n}\n\nexport function usePageTree(): PageTreeContextValue {\n const context = React.useContext(PageTreeContext)\n\n if (!context) {\n return {\n activeDragRowID: null,\n canMoveDocs: false,\n collapsedIDs: new Set<string>(),\n parentMoveEnabled: false,\n pendingMoveRowID: null,\n toggleRow: () => {},\n }\n }\n\n return context\n}\n\nexport function usePageTreeRowDnd(): null | PageTreeRowDndContextValue {\n return React.useContext(PageTreeRowDndContext)\n}\n"],"names":["React","PageTreeContext","createContext","PageTreeRowDndContext","PageTreeProvider","children","value","Provider","PageTreeRowDndProvider","usePageTree","context","useContext","activeDragRowID","canMoveDocs","collapsedIDs","Set","parentMoveEnabled","pendingMoveRowID","toggleRow","usePageTreeRowDnd"],"mappings":"AAAA;;AAIA,OAAOA,WAAW,QAAO;AAkBzB,MAAMC,gCAAkBD,MAAME,aAAa,CAA8B;AACzE,MAAMC,sCAAwBH,MAAME,aAAa,CAAoC;AAErF,OAAO,SAASE,iBAAiB,EAC/BC,QAAQ,EACRC,KAAK,EAIN;IACC,qBAAO,KAACL,gBAAgBM,QAAQ;QAACD,OAAOA;kBAAQD;;AAClD;AAEA,OAAO,SAASG,uBAAuB,EACrCH,QAAQ,EACRC,KAAK,EAIN;IACC,qBAAO,KAACH,sBAAsBI,QAAQ;QAACD,OAAOA;kBAAQD;;AACxD;AAEA,OAAO,SAASI;IACd,MAAMC,UAAUV,MAAMW,UAAU,CAACV;IAEjC,IAAI,CAACS,SAAS;QACZ,OAAO;YACLE,iBAAiB;YACjBC,aAAa;YACbC,cAAc,IAAIC;YAClBC,mBAAmB;YACnBC,kBAAkB;YAClBC,WAAW,KAAO;QACpB;IACF;IAEA,OAAOR;AACT;AAEA,OAAO,SAASS;IACd,OAAOnB,MAAMW,UAAU,CAACR;AAC1B"}
|