payload-nested-docs-page-tree 1.4.5 → 1.5.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.
Files changed (32) hide show
  1. package/README.md +171 -256
  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 +83 -0
  6. package/dist/components/PageTreeListView.server.js +61 -16
  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 +98 -0
  10. package/dist/components/PageTreeStatusBadge.js.map +1 -0
  11. package/dist/components/PageTreeTitleCell.js +6 -3
  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 +17 -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,280 @@
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
25
+ ### Visual hierarchy
55
26
 
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
27
+ ![Hierarchy UI](assets/visual-hierarchy.png)
65
28
 
66
- ## Feature Preview
29
+ ### Page URL paths
67
30
 
68
- ### Visual hierarchy shading
31
+ ![Page URL path preview](assets/live-url-path-preview.png)
69
32
 
70
- <p align="center">
71
- <img alt="Visual hierarchy shading for nested pages" src="assets/visual-hierarchy-shading.png" width="100%" />
72
- </p>
33
+ ## Drag and drop
73
34
 
74
- ### Homepage icon
75
-
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:
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.
124
75
 
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.
76
+ </details>
135
77
 
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.
137
-
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
- })
83
+ import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
84
+
85
+ // At the start of your afterChange hook:
86
+ if (req.context?.[pageTreeMoveContextKey]) return
146
87
  ```
147
88
 
148
- For custom page collection slugs, pass an exact allow-list:
89
+ | Tree operation | Hook with this guard |
90
+ | --- | --- |
91
+ | Sibling reorder or staged move | Skipped |
92
+ | Published move | Runs once for the subtree |
149
93
 
150
- ```ts
151
- nestedDocsPageTreePlugin({
152
- collections: ['page-tree', 'categories'],
153
- homeIndicator: {
154
- collections: ['page-tree'],
155
- },
156
- })
157
- ```
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.
158
95
 
159
- To disable the home icon everywhere:
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.
160
97
 
161
- ```ts
162
- nestedDocsPageTreePlugin({
163
- collections: ['pages'],
164
- homeIndicator: false,
165
- })
166
- ```
98
+ ## Badges
167
99
 
168
- ## Status Badges
100
+ ![Custom status badges](assets/custom-status-badges.png)
169
101
 
170
- The tree view supports three document states:
102
+ | State | Meaning |
103
+ | --- | --- |
104
+ | `published` | Published and up to date |
105
+ | `changed` | Draft with a published version |
106
+ | `draft` | Not published |
171
107
 
172
- - `published`: live and up to date
173
- - `changed`: live, but has unpublished changes
174
- - `draft`: not published
108
+ Override any labels or colors with `badges`. Unspecified values use Payload defaults; custom colors adapt to light and dark themes.
175
109
 
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:
110
+ ### Live and preview links
177
111
 
178
- <p align="center">
179
- <img alt="Custom status badges in the page tree" src="assets/custom-status-badges.png" width="100%" />
180
- </p>
181
112
 
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
- ```
113
+ https://github.com/user-attachments/assets/e5fdb350-4740-4d80-9811-8c23deaf8701
201
114
 
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.
115
+ Enable `badgesLinks` as shown in [Setup](#setup).
203
116
 
204
- ## Configuration
117
+ Published badges open live with a right-side link icon; draft-only badges open preview with a right-side eye icon. Because drafts with a published version has two (live and draft) states, choose:
205
118
 
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.
119
+ | Value | Behavior |
120
+ | --- | --- |
121
+ | `'live'` | Whole badge, including the right-side link icon, opens live |
122
+ | `'preview'` | Whole badge, including the right-side eye icon, opens preview |
123
+ | `'both'` (default) | Body with link icon opens live; eye icon after a separator opens preview |
216
124
 
217
- ## Diagnostics Mode
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.
218
127
 
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:
128
+ All links open in new tabs. Unlinked badges have no icon. Omit `badgesLinks` to keep badges unlinked, even with preview configured.
220
129
 
221
- <p align="center">
222
- <img alt="Diagnostics mode structured logs" src="assets/diagnostics-mode-logs.png" width="100%" />
223
- </p>
130
+ `badgesLinks.showIcons` defaults to `true`: live links use Payload's chain-link `LinkIcon`, and preview links use `EyeIcon`. Set it to `false` to hide inline badge icons; in `'both'` mode, the separate preview action still shows `ExternalLinkIcon` after the separator.
224
131
 
225
- ```ts
226
- nestedDocsPageTreePlugin({
227
- collections: ['pages'],
228
- diagnostics: true,
229
- })
230
- ```
132
+ <details>
133
+ <summary>URL resolution and unavailable links</summary>
231
134
 
232
- Or with a custom sink:
135
+ 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.
233
136
 
234
- ```ts
235
- nestedDocsPageTreePlugin({
236
- collections: ['pages'],
237
- diagnostics: {
238
- enabled: true,
239
- logger: (event) => req.payload.logger.info({ pageTree: event }),
240
- },
241
- })
242
- ```
137
+ 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.
243
138
 
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:
139
+ </details>
245
140
 
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
141
+ ## Homepage icon
249
142
 
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.
143
+ ![Homepage icon](assets/homepage-icon.png)
251
144
 
252
- Diagnostics is opt-in and adds extra reads per operation. Leave it off in production unless you are actively investigating.
145
+ The homepage icon marks a **root page with slug `home`**. For custom collection slugs, use `homeIndicator: { collections: ['page-tree'] }`.
253
146
 
254
- ## Drag-And-Drop Is Triggering A Deploy?
147
+ ## Diagnostics
255
148
 
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.
149
+ ![Diagnostics logs](assets/diagnostics-mode-logs.png)
257
150
 
258
- ### When you can skip this section
151
+ 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.
259
152
 
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.).
153
+ <details>
154
+ <summary>Log fields and custom logger</summary>
262
155
 
263
- ### When you need the one-line fix
156
+ Events are tagged `[payload-nested-docs-page-tree]` and identify the move, reorder, or change-hook step. Key fields:
264
157
 
265
- You need the fix if **you** wrote an `afterChange` hook that calls something external or expensive on every save. Common cases:
158
+ - `flow`: shared ID for one operation.
159
+ - `publishedMainRowBefore` / `publishedMainRowAfter`: published-row snapshots (`draft: false`).
160
+ - `before` / `after` / `changed`: projected status, parent, and order diffs.
266
161
 
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).
162
+ A published row losing its `published` status produces a `page-tree-change:status-flip` warning.
272
163
 
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.
164
+ ```ts
165
+ diagnostics: {
166
+ enabled: true,
167
+ logger: (event) => console.log(event), // send to your preferred logger
168
+ },
169
+ ```
274
170
 
275
- ### The fix
171
+ </details>
276
172
 
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:
173
+ ## Setup
278
174
 
279
- ```ts
280
- import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
175
+ 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.
281
176
 
282
- // at the top of your afterChange hook:
283
- if (req.context?.[pageTreeMoveContextKey]) return
177
+ ```bash
178
+ pnpm add payload-nested-docs-page-tree
284
179
  ```
285
180
 
286
- The rule is simply **rebuild when the live site changed**, so this one line stays correct in every configuration:
181
+ Register it **after** your existing nested docs plugin. This example shows every page-tree option, including optional badge styling and links:
287
182
 
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 |
183
+ ```ts
184
+ import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
185
+ import { nestedDocsPageTreePlugin } from 'payload-nested-docs-page-tree'
293
186
 
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.
187
+ export const plugins = [
188
+ nestedDocsPlugin({ // Register first; keep your existing nested docs options.
189
+ collections: ['pages'],
190
+ }),
191
+ nestedDocsPageTreePlugin({
192
+ collections: ['pages'], // Required: collections to show as a tree.
193
+ defaultLimit: 100, // Documents per list page.
194
+ hideBreadcrumbs: true, // False to show the read-only breadcrumbs field.
195
+ disabled: false, // True to disable the plugin.
196
+
197
+ homeIndicator: { collections: ['pages'] }, // Mark the root "home" page; false disables it.
198
+
199
+ badges: { // Omit to use Payload defaults.
200
+ labels: { // Override any status label.
201
+ published: 'Live',
202
+ changed: 'Has Changes', // A draft with a published version.
203
+ draft: 'Draft',
204
+ },
205
+ colors: { // Base colors adapt to light and dark themes.
206
+ published: '#bbf3b0',
207
+ changed: '#b9eaf3',
208
+ draft: '#f8d5a7',
209
+ },
210
+ },
295
211
 
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.
212
+ badgesLinks: { // Omit to disable links; preview uses collection admin.preview.
213
+ showIcons: true, // Default; false hides inline icons and keeps the split preview action.
214
+ draftHasPublishedVersion: 'both', // 'live' | 'preview' | 'both'
215
+ liveURL: 'https://www.example.com', // Base URL for the published breadcrumb path.
216
+ },
297
217
 
298
- This goes in **your** hook - the one that calls the deploy webhook. Not in any of the template's stock files.
218
+ publishOnMove: false, // True publishes moves only for pages without pending edits.
219
+ diagnostics: false, // True to trace moves, reorders, and status changes.
220
+ }),
221
+ ]
222
+ ```
299
223
 
300
- Full example:
224
+ 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
225
 
302
- ```ts
303
- import type { CollectionAfterChangeHook } from 'payload'
226
+ 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
227
 
305
- import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
228
+ Refresh the admin import map:
306
229
 
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
- }
230
+ ```bash
231
+ pnpm exec payload generate:importmap
321
232
  ```
322
233
 
323
- See `dev/lib/rebuild.ts` for the full Cloudflare deploy hook example used by the dev playground.
234
+ ## Configuration
235
+
236
+ | Option | Default | Purpose |
237
+ | --- | --- | --- |
238
+ | `collections` | Required | Target collection slugs |
239
+ | `parentFieldSlug` | `'parent'` | Nested docs parent field |
240
+ | `breadcrumbsFieldSlug` | `'breadcrumbs'` | Nested docs breadcrumbs field |
241
+ | `defaultLimit` | `100` | List page size |
242
+ | `hideBreadcrumbs` | `true` | Hide the read-only breadcrumbs field |
243
+ | `homeIndicator` | `{ collections: ['pages'] }` | Collections showing a home icon; `false` disables it |
244
+ | `badges` | Payload defaults | Status label and color overrides |
245
+ | `badgesLinks` | Disabled | Live and preview links |
246
+ | `publishOnMove` | `false` | Publish moves for pages without pending edits |
247
+ | `diagnostics` | `false` | Structured operation logs |
248
+ | `disabled` | `false` | Disable the plugin |
324
249
 
325
250
  ## Development
326
251
 
327
- For local plugin development, use the internal `dev/` app:
252
+ <details>
253
+ <summary>Local playground, checks, and release validation</summary>
254
+
255
+ Source lives in `src/`; the playground lives in `dev/`.
328
256
 
329
257
  ```bash
330
258
  pnpm install
331
259
  pnpm dev
332
- pnpm generate:types
333
- pnpm generate:importmap
334
260
  ```
335
261
 
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:
262
+ 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
263
 
347
264
  ```bash
265
+ pnpm generate:types
266
+ pnpm generate:importmap
348
267
  pnpm test:int
349
268
  pnpm exec tsc --noEmit
350
269
  ```
351
270
 
352
- ## Test in Another Project
353
-
354
- For release validation, test the packed artifact instead of a live source-folder dependency:
271
+ For release validation, test the packed artifact in a consumer project:
355
272
 
356
273
  ```bash
357
274
  pnpm build
358
275
  pnpm pack
359
- ```
360
-
361
- Then in the external consumer app:
362
-
363
- ```bash
276
+ # In the consumer project:
364
277
  pnpm add /path/payload-nested-docs-page-tree-*.tgz
365
278
  ```
279
+
280
+ </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,