payload-nested-docs-page-tree 1.4.4 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.md +169 -257
  2. package/dist/components/PageTreeListView.client.d.ts +3 -2
  3. package/dist/components/PageTreeListView.client.js +11 -31
  4. package/dist/components/PageTreeListView.client.js.map +1 -1
  5. package/dist/components/PageTreeListView.module.css +63 -0
  6. package/dist/components/PageTreeListView.server.js +62 -15
  7. package/dist/components/PageTreeListView.server.js.map +1 -1
  8. package/dist/components/PageTreeStatusBadge.d.ts +6 -0
  9. package/dist/components/PageTreeStatusBadge.js +77 -0
  10. package/dist/components/PageTreeStatusBadge.js.map +1 -0
  11. package/dist/components/PageTreeTitleCell.js +3 -2
  12. package/dist/components/PageTreeTitleCell.js.map +1 -1
  13. package/dist/endpoints/createMovePageEndpoint.js +13 -14
  14. package/dist/endpoints/createMovePageEndpoint.js.map +1 -1
  15. package/dist/endpoints/createReorderPageEndpoint.js +3 -4
  16. package/dist/endpoints/createReorderPageEndpoint.js.map +1 -1
  17. package/dist/endpoints/shared.d.ts +1 -14
  18. package/dist/endpoints/shared.js +3 -6
  19. package/dist/endpoints/shared.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.js +3 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/types.d.ts +15 -1
  24. package/dist/types.js.map +1 -1
  25. package/dist/utilities/badgeLinks.d.ts +29 -0
  26. package/dist/utilities/badgeLinks.js +47 -0
  27. package/dist/utilities/badgeLinks.js.map +1 -0
  28. package/dist/utilities/diagnostics.js +1 -1
  29. package/dist/utilities/diagnostics.js.map +1 -1
  30. package/dist/utilities/pageTreeConfig.js +1 -0
  31. package/dist/utilities/pageTreeConfig.js.map +1 -1
  32. package/package.json +1 -1
package/README.md CHANGED
@@ -1,365 +1,277 @@
1
1
  # payload-nested-docs-page-tree
2
2
 
3
- Companion admin plugin for [`@payloadcms/plugin-nested-docs`](https://payloadcms.com/docs/plugins/nested-docs).
3
+ Page management tools for Payload admin, built on [`@payloadcms/plugin-nested-docs`](https://payloadcms.com/docs/plugins/nested-docs).
4
4
 
5
- <p align="center">
6
- <img alt="Page tree admin overview" src="assets/page-tree-admin-overview.png" width="100%" />
7
- </p>
5
+ - **Page tree UI** with hierarchy and page URL paths.
6
+ - **Intuitive drag and drop** to reorder siblings or move pages between parents.
7
+ - **Status badges** for published pages, drafts, and drafts with unpublished changes.
8
+ - **Live and preview links** from badges, with separate destinations for changed pages.
9
+ - **Customizable badges** with label and color overrides for light and dark themes.
10
+ - **Homepage icon** to identify the root `home` page.
11
+ - **Publishing controls** to stage hierarchy changes or publish eligible moves immediately.
12
+ - **Diagnostics mode** to trace moves, reorders, and published-state changes.
13
+ - **Native list tools** including sorting, filters, pagination, bulk selection, and row actions.
8
14
 
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.
15
+ **Coming soon:** custom badges for any cell.
10
16
 
11
- It works alongside `@payloadcms/plugin-nested-docs`. It does not replace nested docs persistence, breadcrumbs generation, or routing.
17
+ [Page tree](#page-tree-ui) · [Drag and drop](#drag-and-drop) · [Badges](#badges) · [Live and preview links](#live-and-preview-links) · [Homepage icon](#homepage-icon) · [Diagnostics](#diagnostics) · [Full setup](#setup) · [Configuration](#configuration)
12
18
 
13
- Tested with Payload `3.81` and Next.js `16.2`.
19
+ ## Page tree UI
14
20
 
21
+ ![Page tree admin overview](assets/page-tree-admin-overview.png)
15
22
 
16
- ## Install
23
+ Browse nested pages while keeping Payload's sorting, filters, pagination, bulk selection, and row actions.
17
24
 
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
- These fields may live inside presentational containers — tabs, rows, collapsibles, and unnamed groups — because Payload still stores them at the top level of the document. Fields inside a **named** tab or **named** group are nested in the data and are not supported.
31
-
32
- Add `nestedDocsPageTreePlugin(...)` right after `nestedDocsPlugin(...)`:
33
-
34
- ```ts
35
- import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
36
- import { nestedDocsPageTreePlugin } from 'payload-nested-docs-page-tree'
37
-
38
- export const plugins = [
39
- nestedDocsPlugin({
40
- // your existing nested docs config
41
- }),
42
- nestedDocsPageTreePlugin({
43
- collections: ['pages'],
44
- }),
45
- ]
46
- ```
47
-
48
- Refresh the admin import map:
49
-
50
- ```bash
51
- payload generate:importmap
52
- ```
53
-
54
- ## What It Adds
55
-
56
- - replaces the collection list view with a nested tree table
57
- - adds visual hierarchy shading for nested pages
58
- - shows the live URL path for each page
59
- - preserves sorting, filters, pagination, bulk selection, and row actions
60
- - adds `POST /:id/move` for intentional parent changes
61
- - adds `POST /:id/reorder` for orderable-only sibling reordering
62
- - keeps reorder drags scoped to the same parent or root level
63
- - marks the root page with slug `home` using a home icon on the title link
64
- - hides the read-only breadcrumbs field by default
25
+ ### Visual hierarchy
65
26
 
66
- ## Feature Preview
27
+ ![Hierarchy UI](assets/visual-hierarchy.png)
67
28
 
68
- ### Visual hierarchy shading
29
+ ### Page URL paths
69
30
 
70
- <p align="center">
71
- <img alt="Visual hierarchy shading for nested pages" src="assets/visual-hierarchy-shading.png" width="100%" />
72
- </p>
31
+ ![Page URL path preview](assets/live-url-path-preview.png)
73
32
 
74
- ### Homepage icon
33
+ ## Drag and drop
75
34
 
76
- <p align="center">
77
- <img alt="Homepage icon in the page tree" src="assets/homepage-icon.png" width="100%" />
78
- </p>
79
-
80
- ### Live URL path preview
81
-
82
- <p align="center">
83
- <img alt="Live URL path preview in the page tree" src="assets/live-url-path-preview.png" width="100%" />
84
- </p>
85
-
86
- ## Drag-And-Drop Modes
87
-
88
- Parent moves and orderable reordering are separate interactions.
89
-
90
- 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.
35
+ ### Reorder siblings
91
36
 
92
37
  https://github.com/user-attachments/assets/b25ffa1a-a6bd-45cf-bce8-56ba6cdf7e72
93
38
 
39
+ Requires Payload `orderable` and sorting by its order field. Drag the reorder handle within the same parent or root level; only the order key changes.
94
40
 
95
-
96
- 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.
97
-
98
- Edit hierarchy mode
41
+ ### Edit hierarchy
99
42
 
100
43
  https://github.com/user-attachments/assets/618d5e53-5918-40be-9932-0f516e5e82ba
101
44
 
102
- Parent move to child
45
+ Enable **Edit Hierarchy** to move pages between parents using Payload's API and nested docs hooks.
46
+
47
+ ### Move to a parent
103
48
 
104
49
  https://github.com/user-attachments/assets/4cb25109-e515-4955-8503-39fddac0020f
105
50
 
106
- Parent move back to root
51
+ Drop a page onto another page to make it a child.
52
+
53
+ ### Move back to root
107
54
 
108
55
  https://github.com/user-attachments/assets/470cb5b3-61c8-4b5b-a6e4-5e855f58a0e4
109
56
 
110
- Same-parent reorder guard
57
+ Drop between root pages to return a page to the root level.
111
58
 
112
- https://github.com/user-attachments/assets/2513ff04-192e-4fdf-808f-6004f55d871c
59
+ ### Same-parent reorder guard
113
60
 
61
+ https://github.com/user-attachments/assets/2513ff04-192e-4fdf-808f-6004f55d871c
114
62
 
63
+ Reordering stays within the current parent. Use **Edit Hierarchy** to change parents.
115
64
 
65
+ Moves are staged as drafts by default. Set `publishOnMove: true` to publish a move immediately **only when the page has no pending edits**. Collections without drafts always move live.
116
66
 
117
- 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.
67
+ <details>
68
+ <summary>Publishing and locales</summary>
118
69
 
119
70
  ### Publishing moves
120
71
 
121
- 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.
122
-
123
- Set `publishOnMove: true` to publish a move as soon as it happens — but only when it is safe to:
124
-
125
- ```ts
126
- nestedDocsPageTreePlugin({
127
- collections: ['pages'],
128
- publishOnMove: true,
129
- })
130
- ```
131
-
132
- 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.
133
-
134
- 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.
72
+ - Staged moves update the tree immediately; live paths change on publication.
73
+ - With `publishOnMove: true`, pages with pending edits still stay staged. Publishing a move also republishes descendants so their live URLs follow the new parent.
74
+ - On localized collections, the parent is shared but breadcrumbs are localized. Publishing a move updates breadcrumbs only in the active locale; other locales retain their previous URLs until published. Leave `publishOnMove` off if this does not suit your routing.
135
75
 
136
- 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.
76
+ </details>
137
77
 
138
- ## Home Indicator
78
+ ## Drag-And-Drop Is Triggering A Deploy?
139
79
 
140
- By default, the home icon is enabled only for the `pages` collection.
80
+ If your `afterChange` hook triggers external work-deploys, notifications, or search indexing-skip tree writes that leave the published site unchanged:
141
81
 
142
82
  ```ts
143
- nestedDocsPageTreePlugin({
144
- collections: ['pages'],
145
- })
146
- ```
147
-
148
- For custom page collection slugs, pass an exact allow-list:
83
+ import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
149
84
 
150
- ```ts
151
- nestedDocsPageTreePlugin({
152
- collections: ['page-tree', 'categories'],
153
- homeIndicator: {
154
- collections: ['page-tree'],
155
- },
156
- })
85
+ // At the start of your afterChange hook:
86
+ if (req.context?.[pageTreeMoveContextKey]) return
157
87
  ```
158
88
 
159
- To disable the home icon everywhere:
89
+ | Tree operation | Hook with this guard |
90
+ | --- | --- |
91
+ | Sibling reorder or staged move | Skipped |
92
+ | Published move | Runs once for the subtree |
160
93
 
161
- ```ts
162
- nestedDocsPageTreePlugin({
163
- collections: ['pages'],
164
- homeIndicator: false,
165
- })
166
- ```
94
+ The same guard works with or without `publishOnMove`. Without it, your hook may run on every drag, including staged changes. Hooks that only invalidate caches generally need no deploy guard.
167
95
 
168
- ## Status Badges
96
+ See [the playground deploy hook](dev/lib/rebuild.ts) for a complete example. The plugin provides `POST /:id/move` and `POST /:id/reorder` endpoints for these interactions.
169
97
 
170
- The tree view supports three document states:
98
+ ## Badges
171
99
 
172
- - `published`: live and up to date
173
- - `changed`: live, but has unpublished changes
174
- - `draft`: not published
100
+ ![Custom status badges](assets/custom-status-badges.png)
175
101
 
176
- 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:
102
+ | State | Meaning |
103
+ | --- | --- |
104
+ | `published` | Published and up to date |
105
+ | `changed` | Draft with a published version |
106
+ | `draft` | Not published |
177
107
 
178
- <p align="center">
179
- <img alt="Custom status badges in the page tree" src="assets/custom-status-badges.png" width="100%" />
180
- </p>
108
+ Override any labels or colors with `badges`. Unspecified values use Payload defaults; custom colors adapt to light and dark themes.
181
109
 
182
- ```ts
183
- nestedDocsPageTreePlugin({
184
- collections: ['pages'],
185
- badges: {
186
- colors: {
187
- // Example: use a custom green / blue / orange palette.
188
- published: '#bbf3b0',
189
- changed: '#b9eaf3',
190
- draft: '#f8d5a7',
191
- },
192
- labels: {
193
- // Example: use custom labels for states.
194
- published: 'Live',
195
- changed: 'Has Changes',
196
- draft: 'Draft Only',
197
- },
198
- },
199
- }),
200
- ```
110
+ ### Live and preview links
201
111
 
202
- `labels` and `colors` are optional partial overrides. Missing entries fall back to the built-in Payload-themed defaults for published, changed, and draft states.
203
112
 
204
- ## Configuration
113
+ https://github.com/user-attachments/assets/e5fdb350-4740-4d80-9811-8c23deaf8701
205
114
 
206
- - `collections`: target collection slugs
207
- - `parentFieldSlug`: defaults to `'parent'`
208
- - `breadcrumbsFieldSlug`: defaults to `'breadcrumbs'`
209
- - `defaultLimit`: defaults to `100`
210
- - `hideBreadcrumbs`: defaults to `true`
211
- - `disabled`: defaults to `false`
212
- - `homeIndicator`: defaults to `{ collections: ['pages'] }`; set to `false` to disable
213
- - `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).
214
- - `badges`: optional label and color overrides for `published`, `changed`, and `draft`
215
- - `diagnostics`: defaults to `false`. Enables structured diagnostic logging for tree-related publish/draft regressions; see below.
115
+ Enable `badgesLinks` as shown in [Setup](#setup).
216
116
 
217
- ## Diagnostics Mode
117
+ Published badges open live; draft-only badges open preview. For drafts with a published version, choose:
218
118
 
219
- 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:
119
+ | Value | Behavior |
120
+ | --- | --- |
121
+ | `'live'` | Badge opens live |
122
+ | `'preview'` | Badge opens preview |
123
+ | `'both'` (default) | Body opens live; right-side icon opens preview |
220
124
 
221
- <p align="center">
222
- <img alt="Diagnostics mode structured logs" src="assets/diagnostics-mode-logs.png" width="100%" />
223
- </p>
125
+ - **Live:** Uses the published document's last breadcrumb URL and `liveURL`. Breadcrumbs must match your frontend routes. Omit `liveURL` for preview links only.
126
+ - **Preview:** Uses the collection's [`admin.preview`](https://payloadcms.com/docs/admin/preview) callback. Your frontend must serve draft content.
224
127
 
225
- ```ts
226
- nestedDocsPageTreePlugin({
227
- collections: ['pages'],
228
- diagnostics: true,
229
- })
230
- ```
128
+ All links open in new tabs. Omit `badgesLinks` to keep badges unlinked, even with preview configured.
231
129
 
232
- Or with a custom sink:
130
+ <details>
131
+ <summary>URL resolution and unavailable links</summary>
233
132
 
234
- ```ts
235
- nestedDocsPageTreePlugin({
236
- collections: ['pages'],
237
- diagnostics: {
238
- enabled: true,
239
- logger: (event) => req.payload.logger.info({ pageTree: event }),
240
- },
241
- })
242
- ```
133
+ Live links use `breadcrumbsFieldSlug` and the published path, unaffected by draft slug or parent changes. Preview receives the latest saved draft, locale, request, and user token. Unsaved changes and `admin.livePreview.url` are not used.
243
134
 
244
- 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:
135
+ Missing breadcrumbs, missing or invalid URLs, or a failing preview callback disable only that link. In `'both'` mode, no live URL leaves the body as text; no preview URL hides the icon. Single-link modes never switch destinations. The selected mode is independent of the badge label.
245
136
 
246
- - `flow`: id shared by every event for one logical operation
247
- - `publishedMainRowBefore` / `publishedMainRowAfter`: fresh reads of the public/published row (`draft: false`)
248
- - `before` / `after` / `changed`: projected diffs for `_status`, the parent field, and the orderable field when present
137
+ </details>
249
138
 
250
- 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.
139
+ ## Homepage icon
251
140
 
252
- Diagnostics is opt-in and adds extra reads per operation. Leave it off in production unless you are actively investigating.
141
+ ![Homepage icon](assets/homepage-icon.png)
253
142
 
254
- ## Drag-And-Drop Is Triggering A Deploy?
143
+ The homepage icon marks a **root page with slug `home`**. For custom collection slugs, use `homeIndicator: { collections: ['page-tree'] }`.
255
144
 
256
- 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.
145
+ ## Diagnostics
257
146
 
258
- ### When you can skip this section
147
+ ![Diagnostics logs](assets/diagnostics-mode-logs.png)
259
148
 
260
- - 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.
261
- - Any setup where your `afterChange` hooks only do in-process cache work (`revalidatePath`, `revalidateTag`, in-memory caches, etc.).
149
+ Set `diagnostics: true` and reproduce the issue. Logs group related events by `flow` and show before/after changes to status, parent, order, and the published row. Diagnostics adds database reads; enable it while investigating.
262
150
 
263
- ### When you need the one-line fix
151
+ <details>
152
+ <summary>Log fields and custom logger</summary>
264
153
 
265
- You need the fix if **you** wrote an `afterChange` hook that calls something external or expensive on every save. Common cases:
154
+ Events are tagged `[payload-nested-docs-page-tree]` and identify the move, reorder, or change-hook step. Key fields:
266
155
 
267
- - **Cloudflare Pages / Netlify / Vercel Deploy Hooks** (`fetch(DEPLOY_HOOK_URL)`) - these trigger full rebuilds and burn build minutes.
268
- - **GitHub Actions** `repository_dispatch` triggers.
269
- - **Manually-invoked SSG rebuilds**.
270
- - **Publish notifications** (email, Slack) on status transitions.
271
- - **Heavy search reindex jobs** (Algolia, Meilisearch full-document push).
156
+ - `flow`: shared ID for one operation.
157
+ - `publishedMainRowBefore` / `publishedMainRowAfter`: published-row snapshots (`draft: false`).
158
+ - `before` / `after` / `changed`: projected status, parent, and order diffs.
272
159
 
273
- 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.
160
+ A published row losing its `published` status produces a `page-tree-change:status-flip` warning.
274
161
 
275
- ### The fix
162
+ ```ts
163
+ diagnostics: {
164
+ enabled: true,
165
+ logger: (event) => console.log(event), // send to your preferred logger
166
+ },
167
+ ```
276
168
 
277
- 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:
169
+ </details>
278
170
 
279
- ```ts
280
- import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
171
+ ## Setup
281
172
 
282
- // at the top of your afterChange hook:
283
- if (req.context?.[pageTreeMoveContextKey]) return
173
+ Tested with Payload `3.81` and Next.js `16.2`. Requires `@payloadcms/plugin-nested-docs`, which continues to own persistence and breadcrumb generation; your frontend owns routing.
174
+
175
+ ```bash
176
+ pnpm add payload-nested-docs-page-tree
284
177
  ```
285
178
 
286
- The rule is simply **rebuild when the live site changed**, so this one line stays correct in every configuration:
179
+ Register it **after** your existing nested docs plugin. This example shows every page-tree option, including optional badge styling and links:
287
180
 
288
- | What the editor did in the tree | Live site changed? | Your deploy hook |
289
- | --- | --- | --- |
290
- | Reordered siblings | no - only the order key was written | does not fire |
291
- | Moved a page, default settings | no - the move is staged as a draft | does not fire |
292
- | Moved a page with [`publishOnMove`](#publishing-moves) on | yes - the page and its descendants have new URLs | fires once |
181
+ ```ts
182
+ import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
183
+ import { nestedDocsPageTreePlugin } from 'payload-nested-docs-page-tree'
293
184
 
294
- 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.
185
+ export const plugins = [
186
+ nestedDocsPlugin({ // Register first; keep your existing nested docs options.
187
+ collections: ['pages'],
188
+ }),
189
+ nestedDocsPageTreePlugin({
190
+ collections: ['pages'], // Required: collections to show as a tree.
191
+ defaultLimit: 100, // Documents per list page.
192
+ hideBreadcrumbs: true, // False to show the read-only breadcrumbs field.
193
+ disabled: false, // True to disable the plugin.
194
+
195
+ homeIndicator: { collections: ['pages'] }, // Mark the root "home" page; false disables it.
196
+
197
+ badges: { // Omit to use Payload defaults.
198
+ labels: { // Override any status label.
199
+ published: 'Live',
200
+ changed: 'Has Changes', // A draft with a published version.
201
+ draft: 'Draft',
202
+ },
203
+ colors: { // Base colors adapt to light and dark themes.
204
+ published: '#bbf3b0',
205
+ changed: '#b9eaf3',
206
+ draft: '#f8d5a7',
207
+ },
208
+ },
295
209
 
296
- To rebuild after **every** page-tree drag, including reorders, just leave the guard out. That also rebuilds on staged moves, which change nothing live.
210
+ badgesLinks: { // Omit to disable links; preview uses collection admin.preview.
211
+ draftHasPublishedVersion: 'both', // 'live' | 'preview' | 'both'
212
+ liveURL: 'https://www.example.com', // Base URL for the published breadcrumb path.
213
+ },
297
214
 
298
- This goes in **your** hook - the one that calls the deploy webhook. Not in any of the template's stock files.
215
+ publishOnMove: false, // True publishes moves only for pages without pending edits.
216
+ diagnostics: false, // True to trace moves, reorders, and status changes.
217
+ }),
218
+ ]
219
+ ```
299
220
 
300
- Full example:
221
+ Only `collections` is required in the page-tree config. Omit `badges` for default styling and `badgesLinks` to disable links. Preview links require the collection's `admin.preview` callback.
301
222
 
302
- ```ts
303
- import type { CollectionAfterChangeHook } from 'payload'
223
+ Each target collection needs parent, breadcrumbs, and `admin.useAsTitle` fields stored at the document's top level. Presentational tabs, rows, collapsibles, and unnamed groups are supported; fields inside named tabs or groups are not.
304
224
 
305
- import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
225
+ Refresh the admin import map:
306
226
 
307
- export const triggerDeployOnPublishedChange: CollectionAfterChangeHook = async ({
308
- doc,
309
- previousDoc,
310
- req,
311
- }) => {
312
- // -- plugin opt-out --
313
- if (req.context?.[pageTreeMoveContextKey]) return
314
-
315
- // -- your deploy logic (example) --
316
- // Fire on publish, republish, or unpublish - every transition the live site cares about.
317
- if (doc._status === 'published' || previousDoc?._status === 'published') {
318
- // POST to your Cloudflare / Netlify / Vercel deploy hook here
319
- }
320
- }
227
+ ```bash
228
+ pnpm exec payload generate:importmap
321
229
  ```
322
230
 
323
- See `dev/lib/rebuild.ts` for the full Cloudflare deploy hook example used by the dev playground.
231
+ ## Configuration
232
+
233
+ | Option | Default | Purpose |
234
+ | --- | --- | --- |
235
+ | `collections` | Required | Target collection slugs |
236
+ | `parentFieldSlug` | `'parent'` | Nested docs parent field |
237
+ | `breadcrumbsFieldSlug` | `'breadcrumbs'` | Nested docs breadcrumbs field |
238
+ | `defaultLimit` | `100` | List page size |
239
+ | `hideBreadcrumbs` | `true` | Hide the read-only breadcrumbs field |
240
+ | `homeIndicator` | `{ collections: ['pages'] }` | Collections showing a home icon; `false` disables it |
241
+ | `badges` | Payload defaults | Status label and color overrides |
242
+ | `badgesLinks` | Disabled | Live and preview links |
243
+ | `publishOnMove` | `false` | Publish moves for pages without pending edits |
244
+ | `diagnostics` | `false` | Structured operation logs |
245
+ | `disabled` | `false` | Disable the plugin |
324
246
 
325
247
  ## Development
326
248
 
327
- For local plugin development, use the internal `dev/` app:
249
+ <details>
250
+ <summary>Local playground, checks, and release validation</summary>
251
+
252
+ Source lives in `src/`; the playground lives in `dev/`.
328
253
 
329
254
  ```bash
330
255
  pnpm install
331
256
  pnpm dev
332
- pnpm generate:types
333
- pnpm generate:importmap
334
257
  ```
335
258
 
336
- Open [http://localhost:3000/admin](http://localhost:3000/admin) and sign in with:
337
-
338
- - email: `admin@email.com`
339
- - password: `password`
340
-
341
- The dev app creates this user automatically on startup. After signing in, use the **seed the database** button on the dashboard to add the sample pages.
342
-
343
- Plugin source is in `src/`. The internal test app is in `dev/`.
344
-
345
- For checks:
259
+ Open [localhost:3000/admin](http://localhost:3000/admin). The playground creates `admin@email.com` / `password` on startup. Use **seed the database** on the dashboard to add sample pages.
346
260
 
347
261
  ```bash
262
+ pnpm generate:types
263
+ pnpm generate:importmap
348
264
  pnpm test:int
349
265
  pnpm exec tsc --noEmit
350
266
  ```
351
267
 
352
- ## Test in Another Project
353
-
354
- For release validation, test the packed artifact instead of a live source-folder dependency:
268
+ For release validation, test the packed artifact in a consumer project:
355
269
 
356
270
  ```bash
357
271
  pnpm build
358
272
  pnpm pack
359
- ```
360
-
361
- Then in the external consumer app:
362
-
363
- ```bash
273
+ # In the consumer project:
364
274
  pnpm add /path/payload-nested-docs-page-tree-*.tgz
365
275
  ```
276
+
277
+ </details>
@@ -1,9 +1,10 @@
1
1
  import type { Column, ListQuery, ListViewClientProps } from 'payload';
2
- import type { NestedDocsPageTreePluginResolvedBadgeConfig, PageTreeSourceDoc } from '../types.js';
2
+ import type { NestedDocsPageTreePluginBadgesLinks, NestedDocsPageTreePluginResolvedBadgeConfig, PageTreeSourceDoc } from '../types.js';
3
3
  import { type PageTreeDoc } from '../utilities/pageTree.js';
4
4
  type PageTreeListViewClientProps = {
5
5
  allDocs: PageTreeDoc[];
6
6
  badgeConfig: NestedDocsPageTreePluginResolvedBadgeConfig;
7
+ badgesLinks?: NestedDocsPageTreePluginBadgesLinks;
7
8
  canMoveDocs: boolean;
8
9
  columnState: Column[];
9
10
  homeIndicatorEnabled: boolean;
@@ -13,5 +14,5 @@ type PageTreeListViewClientProps = {
13
14
  sourceDocs: PageTreeSourceDoc[];
14
15
  useAsTitle: string;
15
16
  } & Omit<ListViewClientProps, 'columnState' | 'Table'>;
16
- export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDocs, columnState, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, query, sourceDocs, useAsTitle, ...props }: PageTreeListViewClientProps): import("react/jsx-runtime").JSX.Element;
17
+ export default function PageTreeListViewClient({ allDocs, badgeConfig, badgesLinks, canMoveDocs, columnState, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, query, sourceDocs, useAsTitle, ...props }: PageTreeListViewClientProps): import("react/jsx-runtime").JSX.Element;
17
18
  export {};
@@ -10,7 +10,7 @@ import { buildInsertDropTargets, getDropTargetParentDoc } from '../utilities/dro
10
10
  import { CANCEL_DRAG_MESSAGE, getDropValidation } from '../utilities/moveValidation.js';
11
11
  import { buildDocSlugPath, buildPageTreeDocs, buildProspectiveDocSlugPath, getDocDisplayLabel, getVisibleTreeDocs } from '../utilities/pageTree.js';
12
12
  import { pageTreeCollisionDetectionStrategy } from '../utilities/pageTreeCollision.js';
13
- import { getPageTreeBadgeColor, getPageTreeBadgeLabel, getPageTreeDisplayStatus } from '../utilities/status.js';
13
+ import { PageTreeStatusBadge } from './PageTreeStatusBadge.js';
14
14
  import { PageTreeProvider, PageTreeRowDndProvider } from './PageTreeContext.js';
15
15
  import styles from './PageTreeListView.module.css';
16
16
  import { PageTreeTitleCell } from './PageTreeTitleCell.js';
@@ -387,35 +387,12 @@ function getDropTargetValidation(args) {
387
387
  parentID: targetDoc.__pageTreeID
388
388
  };
389
389
  }
390
- function getStatusClassName(status) {
391
- if (status === 'changed' || status === 'draft' || status === 'published') {
392
- return status;
393
- }
394
- return 'unknown';
395
- }
396
390
  function renderStatusBadge(args) {
397
- const { badgeConfig, doc, index, t } = args;
398
- const status = getPageTreeDisplayStatus(doc);
399
- const customColor = getPageTreeBadgeColor({
400
- badgeColors: badgeConfig.colors,
401
- status
402
- });
403
- const statusClass = getStatusClassName(status);
404
- const style = customColor ? {
405
- '--page-tree-badge-base': customColor
406
- } : undefined;
407
- return /*#__PURE__*/ _jsx("span", {
408
- className: [
409
- 'pages-hierarchy-status-badge',
410
- `pages-hierarchy-status-badge--${statusClass}`
411
- ].join(' '),
412
- "data-custom-color": customColor ? 'true' : undefined,
413
- style: style,
414
- children: getPageTreeBadgeLabel({
415
- badgeLabels: badgeConfig.labels,
416
- status,
417
- t
418
- })
391
+ const { badgeConfig, badgesLinks, doc, index } = args;
392
+ return /*#__PURE__*/ _jsx(PageTreeStatusBadge, {
393
+ badgeConfig: badgeConfig,
394
+ badgesLinks: badgesLinks,
395
+ doc: doc
419
396
  }, doc.__pageTreeID ?? index);
420
397
  }
421
398
  function ParentMoveToggle({ collectionSlug, enabled, onToggle }) {
@@ -448,7 +425,7 @@ function ParentMoveToggle({ collectionSlug, enabled, onToggle }) {
448
425
  return toggle;
449
426
  }
450
427
  function buildTableColumns(args) {
451
- const { badgeConfig, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
428
+ const { badgeConfig, badgesLinks, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
452
429
  const columnsToUse = columnState.map((column)=>{
453
430
  if (column.accessor === useAsTitle) {
454
431
  return {
@@ -466,6 +443,7 @@ function buildTableColumns(args) {
466
443
  ...column,
467
444
  renderedCells: docs.map((doc, index)=>renderStatusBadge({
468
445
  badgeConfig,
446
+ badgesLinks,
469
447
  doc,
470
448
  index,
471
449
  t
@@ -740,7 +718,7 @@ function HierarchyTable({ activeDragRowID, activeDragType, activeDropTarget, all
740
718
  })
741
719
  });
742
720
  }
743
- export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDocs, columnState, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, query, sourceDocs, useAsTitle, ...props }) {
721
+ export default function PageTreeListViewClient({ allDocs, badgeConfig, badgesLinks, canMoveDocs, columnState, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, query, sourceDocs, useAsTitle, ...props }) {
744
722
  const router = useRouter();
745
723
  const searchParams = useSearchParams();
746
724
  const { config } = useConfig();
@@ -913,6 +891,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
913
891
  ]);
914
892
  const tableColumns = React.useMemo(()=>buildTableColumns({
915
893
  badgeConfig,
894
+ badgesLinks,
916
895
  columnState: paginatedColumnState,
917
896
  docs: displayedPaginatedDocs,
918
897
  enableRowSelections: props.enableRowSelections,
@@ -925,6 +904,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, canMoveDo
925
904
  paginatedColumnState,
926
905
  displayedPaginatedDocs,
927
906
  badgeConfig,
907
+ badgesLinks,
928
908
  homeIndicatorEnabled,
929
909
  orderableFieldName,
930
910
  parentFieldSlug,