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.
- package/README.md +169 -257
- 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 +63 -0
- package/dist/components/PageTreeListView.server.js +62 -15
- package/dist/components/PageTreeListView.server.js.map +1 -1
- package/dist/components/PageTreeStatusBadge.d.ts +6 -0
- package/dist/components/PageTreeStatusBadge.js +77 -0
- package/dist/components/PageTreeStatusBadge.js.map +1 -0
- package/dist/components/PageTreeTitleCell.js +3 -2
- 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 +15 -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,277 @@
|
|
|
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
|
|
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
|
-
|
|
27
|
+

|
|
67
28
|
|
|
68
|
-
###
|
|
29
|
+
### Page URL paths
|
|
69
30
|
|
|
70
|
-
|
|
71
|
-
<img alt="Visual hierarchy shading for nested pages" src="assets/visual-hierarchy-shading.png" width="100%" />
|
|
72
|
-
</p>
|
|
31
|
+

|
|
73
32
|
|
|
74
|
-
|
|
33
|
+
## Drag and drop
|
|
75
34
|
|
|
76
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
+
</details>
|
|
137
77
|
|
|
138
|
-
##
|
|
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
|
-
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
98
|
+
## Badges
|
|
171
99
|
|
|
172
|
-
|
|
173
|
-
- `changed`: live, but has unpublished changes
|
|
174
|
-
- `draft`: not published
|
|
100
|
+

|
|
175
101
|
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
113
|
+
https://github.com/user-attachments/assets/e5fdb350-4740-4d80-9811-8c23deaf8701
|
|
205
114
|
|
|
206
|
-
|
|
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
|
-
|
|
117
|
+
Published badges open live; draft-only badges open preview. For drafts with a published version, choose:
|
|
218
118
|
|
|
219
|
-
|
|
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
|
-
|
|
222
|
-
|
|
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
|
-
|
|
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
|
-
|
|
130
|
+
<details>
|
|
131
|
+
<summary>URL resolution and unavailable links</summary>
|
|
233
132
|
|
|
234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
139
|
+
## Homepage icon
|
|
251
140
|
|
|
252
|
-
|
|
141
|
+

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

|
|
259
148
|
|
|
260
|
-
|
|
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
|
-
|
|
151
|
+
<details>
|
|
152
|
+
<summary>Log fields and custom logger</summary>
|
|
264
153
|
|
|
265
|
-
|
|
154
|
+
Events are tagged `[payload-nested-docs-page-tree]` and identify the move, reorder, or change-hook step. Key fields:
|
|
266
155
|
|
|
267
|
-
-
|
|
268
|
-
-
|
|
269
|
-
-
|
|
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
|
-
|
|
160
|
+
A published row losing its `published` status produces a `page-tree-change:status-flip` warning.
|
|
274
161
|
|
|
275
|
-
|
|
162
|
+
```ts
|
|
163
|
+
diagnostics: {
|
|
164
|
+
enabled: true,
|
|
165
|
+
logger: (event) => console.log(event), // send to your preferred logger
|
|
166
|
+
},
|
|
167
|
+
```
|
|
276
168
|
|
|
277
|
-
|
|
169
|
+
</details>
|
|
278
170
|
|
|
279
|
-
|
|
280
|
-
import { pageTreeMoveContextKey } from 'payload-nested-docs-page-tree'
|
|
171
|
+
## Setup
|
|
281
172
|
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 |
|
|
181
|
+
```ts
|
|
182
|
+
import { nestedDocsPlugin } from '@payloadcms/plugin-nested-docs'
|
|
183
|
+
import { nestedDocsPageTreePlugin } from 'payload-nested-docs-page-tree'
|
|
293
184
|
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
+
Refresh the admin import map:
|
|
306
226
|
|
|
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
|
-
}
|
|
227
|
+
```bash
|
|
228
|
+
pnpm exec payload generate:importmap
|
|
321
229
|
```
|
|
322
230
|
|
|
323
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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 {
|
|
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,
|