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.
- package/README.md +171 -256
- package/dist/components/PageTreeListView.client.d.ts +3 -2
- package/dist/components/PageTreeListView.client.js +11 -31
- package/dist/components/PageTreeListView.client.js.map +1 -1
- package/dist/components/PageTreeListView.module.css +83 -0
- package/dist/components/PageTreeListView.server.js +61 -16
- package/dist/components/PageTreeListView.server.js.map +1 -1
- package/dist/components/PageTreeStatusBadge.d.ts +6 -0
- package/dist/components/PageTreeStatusBadge.js +98 -0
- package/dist/components/PageTreeStatusBadge.js.map +1 -0
- package/dist/components/PageTreeTitleCell.js +6 -3
- package/dist/components/PageTreeTitleCell.js.map +1 -1
- package/dist/endpoints/createMovePageEndpoint.js +13 -14
- package/dist/endpoints/createMovePageEndpoint.js.map +1 -1
- package/dist/endpoints/createReorderPageEndpoint.js +3 -4
- package/dist/endpoints/createReorderPageEndpoint.js.map +1 -1
- package/dist/endpoints/shared.d.ts +1 -14
- package/dist/endpoints/shared.js +3 -6
- package/dist/endpoints/shared.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +17 -1
- package/dist/types.js.map +1 -1
- package/dist/utilities/badgeLinks.d.ts +29 -0
- package/dist/utilities/badgeLinks.js +47 -0
- package/dist/utilities/badgeLinks.js.map +1 -0
- package/dist/utilities/diagnostics.js +1 -1
- package/dist/utilities/diagnostics.js.map +1 -1
- package/dist/utilities/pageTreeConfig.js +1 -0
- package/dist/utilities/pageTreeConfig.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,365 +1,280 @@
|
|
|
1
1
|
# payload-nested-docs-page-tree
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Page management tools for Payload admin, built on [`@payloadcms/plugin-nested-docs`](https://payloadcms.com/docs/plugins/nested-docs).
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
15
|
+
**Coming soon:** custom badges for any cell.
|
|
10
16
|
|
|
11
|
-
|
|
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
|
-
|
|
19
|
+
## Page tree UI
|
|
14
20
|
|
|
21
|
+

|
|
15
22
|
|
|
16
|
-
|
|
23
|
+
Browse nested pages while keeping Payload's sorting, filters, pagination, bulk selection, and row actions.
|
|
17
24
|
|
|
18
|
-
|
|
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
|
-
-
|
|
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
|
+

|
|
65
28
|
|
|
66
|
-
|
|
29
|
+
### Page URL paths
|
|
67
30
|
|
|
68
|
-
|
|
31
|
+

|
|
69
32
|
|
|
70
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
Drop between root pages to return a page to the root level.
|
|
111
58
|
|
|
112
|
-
|
|
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
|
-
|
|
67
|
+
<details>
|
|
68
|
+
<summary>Publishing and locales</summary>
|
|
118
69
|
|
|
119
70
|
### Publishing moves
|
|
120
71
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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
|
-
|
|
137
|
-
|
|
138
|
-
## Home Indicator
|
|
78
|
+
## Drag-And-Drop Is Triggering A Deploy?
|
|
139
79
|
|
|
140
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
nestedDocsPageTreePlugin({
|
|
163
|
-
collections: ['pages'],
|
|
164
|
-
homeIndicator: false,
|
|
165
|
-
})
|
|
166
|
-
```
|
|
98
|
+
## Badges
|
|
167
99
|
|
|
168
|
-
|
|
100
|
+

|
|
169
101
|
|
|
170
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
+
Enable `badgesLinks` as shown in [Setup](#setup).
|
|
203
116
|
|
|
204
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
collections: ['pages'],
|
|
228
|
-
diagnostics: true,
|
|
229
|
-
})
|
|
230
|
-
```
|
|
132
|
+
<details>
|
|
133
|
+
<summary>URL resolution and unavailable links</summary>
|
|
231
134
|
|
|
232
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
+
</details>
|
|
245
140
|
|
|
246
|
-
|
|
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
|
-
|
|
143
|
+

|
|
251
144
|
|
|
252
|
-
|
|
145
|
+
The homepage icon marks a **root page with slug `home`**. For custom collection slugs, use `homeIndicator: { collections: ['page-tree'] }`.
|
|
253
146
|
|
|
254
|
-
##
|
|
147
|
+
## Diagnostics
|
|
255
148
|
|
|
256
|
-
|
|
149
|
+

|
|
257
150
|
|
|
258
|
-
|
|
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
|
-
|
|
261
|
-
|
|
153
|
+
<details>
|
|
154
|
+
<summary>Log fields and custom logger</summary>
|
|
262
155
|
|
|
263
|
-
|
|
156
|
+
Events are tagged `[payload-nested-docs-page-tree]` and identify the move, reorder, or change-hook step. Key fields:
|
|
264
157
|
|
|
265
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
+
```ts
|
|
165
|
+
diagnostics: {
|
|
166
|
+
enabled: true,
|
|
167
|
+
logger: (event) => console.log(event), // send to your preferred logger
|
|
168
|
+
},
|
|
169
|
+
```
|
|
274
170
|
|
|
275
|
-
|
|
171
|
+
</details>
|
|
276
172
|
|
|
277
|
-
|
|
173
|
+
## Setup
|
|
278
174
|
|
|
279
|
-
|
|
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
|
-
|
|
283
|
-
|
|
177
|
+
```bash
|
|
178
|
+
pnpm add payload-nested-docs-page-tree
|
|
284
179
|
```
|
|
285
180
|
|
|
286
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
228
|
+
Refresh the admin import map:
|
|
306
229
|
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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 {
|
|
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
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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,
|