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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +148 -11
  2. package/dist/components/PageTreeListView.client.js +9 -4
  3. package/dist/components/PageTreeListView.client.js.map +1 -1
  4. package/dist/components/PageTreeListView.module.css +55 -8
  5. package/dist/components/PageTreeListView.server.js +30 -14
  6. package/dist/components/PageTreeListView.server.js.map +1 -1
  7. package/dist/components/PageTreeLocaleStatusBadges.d.ts +7 -0
  8. package/dist/components/PageTreeLocaleStatusBadges.js +79 -0
  9. package/dist/components/PageTreeLocaleStatusBadges.js.map +1 -0
  10. package/dist/components/PageTreeStatusBadge.d.ts +2 -1
  11. package/dist/components/PageTreeStatusBadge.js +37 -7
  12. package/dist/components/PageTreeStatusBadge.js.map +1 -1
  13. package/dist/components/PageTreeTitleCell.js +12 -10
  14. package/dist/components/PageTreeTitleCell.js.map +1 -1
  15. package/dist/index.d.ts +1 -1
  16. package/dist/index.js +8 -4
  17. package/dist/index.js.map +1 -1
  18. package/dist/types.d.ts +49 -8
  19. package/dist/types.js.map +1 -1
  20. package/dist/utilities/badgeConfig.js +3 -0
  21. package/dist/utilities/badgeConfig.js.map +1 -1
  22. package/dist/utilities/badgeLinks.d.ts +1 -1
  23. package/dist/utilities/badgeLinks.js +13 -3
  24. package/dist/utilities/badgeLinks.js.map +1 -1
  25. package/dist/utilities/localeStatus.d.ts +11 -0
  26. package/dist/utilities/localeStatus.js +83 -0
  27. package/dist/utilities/localeStatus.js.map +1 -0
  28. package/dist/utilities/pageTree.js +1 -1
  29. package/dist/utilities/pageTree.js.map +1 -1
  30. package/dist/utilities/pageTreeConfig.d.ts +1 -1
  31. package/dist/utilities/pageTreeConfig.js +2 -0
  32. package/dist/utilities/pageTreeConfig.js.map +1 -1
  33. package/dist/utilities/status.d.ts +2 -2
  34. package/dist/utilities/status.js.map +1 -1
  35. package/package.json +1 -1
package/README.md CHANGED
@@ -5,6 +5,7 @@ Page management tools for Payload admin, built on [`@payloadcms/plugin-nested-do
5
5
  - **Page tree UI** with hierarchy and page URL paths.
6
6
  - **Intuitive drag and drop** to reorder siblings or move pages between parents.
7
7
  - **Status badges** for published pages, drafts, and drafts with unpublished changes.
8
+ - **Localized status badges** to see each language's publication status and open the page editor in that language with one click.
8
9
  - **Live and preview links** from badges, with separate destinations for changed pages.
9
10
  - **Customizable badges** with label and color overrides for light and dark themes.
10
11
  - **Homepage icon** to identify the root `home` page.
@@ -14,7 +15,7 @@ Page management tools for Payload admin, built on [`@payloadcms/plugin-nested-do
14
15
 
15
16
  **Coming soon:** custom badges for any cell.
16
17
 
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)
18
+ [Page tree](#page-tree-ui) · [Drag and drop](#drag-and-drop) · [Badges](#badges) · [Localized status](#locale-status-badges) · [Live and preview links](#live-and-preview-links) · [Homepage icon](#homepage-icon) · [Diagnostics](#diagnostics) · [Full setup](#setup) · [Configuration](#configuration)
18
19
 
19
20
  ## Page tree UI
20
21
 
@@ -107,6 +108,81 @@ See [the playground deploy hook](dev/lib/rebuild.ts) for a complete example. The
107
108
 
108
109
  Override any labels or colors with `badges`. Unspecified values use Payload defaults; custom colors adapt to light and dark themes.
109
110
 
111
+ ### Locale status badges
112
+
113
+ ![Localized page status badges for English, French, and German, with French selected](assets/localized-status.png)
114
+
115
+ See each page's publication status across languages directly in the page tree. Each language has its own color-coded badge showing whether the page is published, a draft, or has unpublished changes. Click a language badge to open that page's editor in the selected language, in the same tab.
116
+
117
+ The active language also shows its status label, such as **FR · Published** or **FR · Changed**. Other languages show their codes, such as **EN** and **DE**, with their own status colors. Badges line up across rows so editors can quickly scan for translations that need attention.
118
+
119
+ Set `badges.locales: true` to show one badge for each available locale:
120
+
121
+ ```ts
122
+ nestedDocsPageTreePlugin({
123
+ collections: ['pages'],
124
+ badges: {
125
+ locales: true,
126
+ // Existing colors and labels apply independently to each status.
127
+ labels: { changed: 'Unpublished edits' },
128
+ },
129
+ })
130
+ ```
131
+
132
+ For an editor using French, the badges appear side by side: **EN** | **FR · Unpublished edits** | **DE**.
133
+
134
+ Locale codes display in uppercase and follow the order in Payload's localization configuration, respecting `filterAvailableLocales`. Existing badge label and color overrides apply to each locale's status.
135
+
136
+ Badge links support keyboard navigation, include the language and status in their accessible names, and respect your configured admin route. Hidden badges preserve their aligned slots and cannot be clicked. Locale badges link to the editor independently of `badgesLinks`, which controls live and preview links on single status badges.
137
+
138
+ This requires Payload's native localized status, configured separately:
139
+
140
+ ```ts
141
+ // Main Payload config
142
+ experimental: { localizeStatus: true },
143
+ localization: {
144
+ defaultLocale: 'en',
145
+ locales: ['en', 'fr', 'de'],
146
+ },
147
+
148
+ // Target collection
149
+ versions: {
150
+ drafts: { localizeStatus: true },
151
+ },
152
+ ```
153
+
154
+ Localized status is experimental in the supported Payload versions. Follow [Payload's status-localization and migration guidance](https://payloadcms.com/docs/configuration/localization#status-localization) before enabling it for existing data. The plugin does not enable it or migrate your database. Collections without localized status, or with `badges.locales` omitted, retain the existing single badge.
155
+
156
+ Statuses are read from the latest draft and current document, with locale fallback disabled:
157
+
158
+ | Latest locale status | Current locale status | Badge |
159
+ | --- | --- | --- |
160
+ | Published | Any | Published |
161
+ | Draft | Published | Unpublished edits |
162
+ | Draft | Draft or missing | Draft |
163
+ | Missing or unsupported | Any | Unknown (neutral) |
164
+
165
+ The plugin batches the two status reads across the tree result, respects read permissions, and does not fetch content separately for every badge.
166
+
167
+ #### Custom appearance
168
+
169
+ The plugin keeps the status calculation and layout reusable. Use these styling hooks to apply a project-specific appearance, such as seasonal colors and status icons:
170
+
171
+ - `.pages-hierarchy-locale-statuses`: the badge group.
172
+ - `.pages-hierarchy-locale-status-badge`: a badge, with `data-locale`, `data-status`, and `data-active`.
173
+ - `.pages-hierarchy-locale-status-badge__code`, `__separator`, and `__label`: the visible pieces.
174
+
175
+ For example, local admin CSS can assign a season color:
176
+
177
+ ```css
178
+ .pages-hierarchy-locale-statuses .pages-hierarchy-locale-status-badge[data-locale='summer'] {
179
+ background: #fff0a8;
180
+ color: #594600;
181
+ }
182
+ ```
183
+
184
+ A reskin can hide `__code` and `__separator` and add a status icon using the badge's `::before` and `data-status`, keeping the active status label and accessible name. The default plugin supplies no icons.
185
+
110
186
  ### Live and preview links
111
187
 
112
188
 
@@ -114,25 +190,50 @@ https://github.com/user-attachments/assets/e5fdb350-4740-4d80-9811-8c23deaf8701
114
190
 
115
191
  Enable `badgesLinks` as shown in [Setup](#setup).
116
192
 
117
- Published badges open live; draft-only badges open preview. For drafts with a published version, choose:
193
+ 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:
118
194
 
119
195
  | Value | Behavior |
120
196
  | --- | --- |
121
- | `'live'` | Badge opens live |
122
- | `'preview'` | Badge opens preview |
123
- | `'both'` (default) | Body opens live; right-side icon opens preview |
197
+ | `'live'` | Whole badge, including the right-side link icon, opens live |
198
+ | `'preview'` | Whole badge, including the right-side eye icon, opens preview |
199
+ | `'both'` (default) | Body with link icon opens live; eye icon after a separator opens preview |
124
200
 
125
- - **Live:** Uses the published document's last breadcrumb URL and `liveURL`. Breadcrumbs must match your frontend routes. Omit `liveURL` for preview links only.
201
+ - **Live:** A string `liveURL` is the base URL for the published document's last breadcrumb path. Alternatively, pass a sync or async callback returning the final URL for tenant domains or localized routes. Omit `liveURL` for preview links only.
126
202
  - **Preview:** Uses the collection's [`admin.preview`](https://payloadcms.com/docs/admin/preview) callback. Your frontend must serve draft content.
127
203
 
128
- All links open in new tabs. Omit `badgesLinks` to keep badges unlinked, even with preview configured.
204
+ All live and preview links open in new tabs. Unlinked badges have no icon. Omit `badgesLinks` to keep single status badges unlinked, even with preview configured. Locale badges still link to the editor.
205
+
206
+ `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.
207
+
208
+ For example, with a `tenant` relationship on pages and a `domain` field on tenants:
209
+
210
+ ```ts
211
+ badgesLinks: {
212
+ liveURL: async ({ doc, locale, path, req }) => {
213
+ if (!path || (typeof doc.tenant !== 'string' && typeof doc.tenant !== 'number')) {
214
+ return null
215
+ }
216
+
217
+ const tenant = await req.payload.findByID({
218
+ collection: 'tenants',
219
+ id: doc.tenant,
220
+ overrideAccess: false,
221
+ req,
222
+ })
223
+
224
+ return new URL(`${locale ? `/${locale}` : ''}${path}`, `https://${tenant.domain}`).href
225
+ },
226
+ },
227
+ ```
228
+
229
+ The callback receives `{ collectionSlug, doc, locale, path, req }`. `doc` is the readable published document at depth `0`, so relationships contain IDs. `locale` is the selected admin locale, or `undefined` without localization. `path` is the last published breadcrumb URL from `breadcrumbsFieldSlug`, or `undefined` when unavailable. The callback can build its own route without breadcrumbs. It runs only when a live destination is needed and a readable published document exists, and stays on the server. The `PageTreeLiveURL` type is exported for standalone callbacks.
129
230
 
130
231
  <details>
131
232
  <summary>URL resolution and unavailable links</summary>
132
233
 
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.
234
+ String live links use `breadcrumbsFieldSlug` and the published path, unaffected by draft slug or parent changes. Callback results are used as final URLs without appending the breadcrumb path. Relative callback URLs resolve against the CMS request URL; return an absolute URL for a separate frontend. Only HTTP and HTTPS links are accepted. Preview receives the latest saved draft, locale, request, and user token. Unsaved changes and `admin.livePreview.url` are not used.
134
235
 
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.
236
+ Missing breadcrumbs for string live URLs, missing or invalid URLs, or a failing live or preview callback disable only that link. Return `null`, `undefined`, or an empty string from the live callback to leave the live destination unlinked. 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.
136
237
 
137
238
  </details>
138
239
 
@@ -208,6 +309,7 @@ export const plugins = [
208
309
  },
209
310
 
210
311
  badgesLinks: { // Omit to disable links; preview uses collection admin.preview.
312
+ showIcons: true, // Default; false hides inline icons and keeps the split preview action.
211
313
  draftHasPublishedVersion: 'both', // 'live' | 'preview' | 'both'
212
314
  liveURL: 'https://www.example.com', // Base URL for the published breadcrumb path.
213
315
  },
@@ -218,7 +320,7 @@ export const plugins = [
218
320
  ]
219
321
  ```
220
322
 
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.
323
+ Only `collections` is required in the page-tree config. Omit `badges` for default styling and `badgesLinks` to disable live and preview links. Preview links require the collection's `admin.preview` callback.
222
324
 
223
325
  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.
224
326
 
@@ -238,7 +340,7 @@ pnpm exec payload generate:importmap
238
340
  | `defaultLimit` | `100` | List page size |
239
341
  | `hideBreadcrumbs` | `true` | Hide the read-only breadcrumbs field |
240
342
  | `homeIndicator` | `{ collections: ['pages'] }` | Collections showing a home icon; `false` disables it |
241
- | `badges` | Payload defaults | Status label and color overrides |
343
+ | `badges` | Payload defaults | Status labels, colors, and optional locale badges |
242
344
  | `badgesLinks` | Disabled | Live and preview links |
243
345
  | `publishOnMove` | `false` | Publish moves for pages without pending edits |
244
346
  | `diagnostics` | `false` | Structured operation logs |
@@ -258,6 +360,18 @@ pnpm dev
258
360
 
259
361
  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.
260
362
 
363
+ The Seed button populates three collections: **Pages** and **Tabbed Pages** use shared publication status; **Localized Pages** uses independent English/French/German status (`en`, `fr`, `de`, in that order). All three use the same 30-page tree, titles, slugs, and ordering in every language. Localized Pages includes mixed published, changed, and draft statuses for testing. Restart the playground after changing locale configuration, then click **seed the database** to populate all three languages. **Reseeding replaces all content and version history in these three playground collections**, removing stale pages and restoring the shared demo hierarchy. Users are preserved.
364
+
365
+ For a fresh in-memory playground database on Windows:
366
+
367
+ ```powershell
368
+ $env:PAYLOAD_TEST_DATABASE = 'true'
369
+ pnpm.cmd dev
370
+ ```
371
+
372
+ Without that flag, normal development uses `DATABASE_URL` from `dev/.env`. The two status modes can coexist in either database setup; no migration is needed for the new Localized Pages collection.
373
+
374
+
261
375
  ```bash
262
376
  pnpm generate:types
263
377
  pnpm generate:importmap
@@ -275,3 +389,26 @@ pnpm add /path/payload-nested-docs-page-tree-*.tgz
275
389
  ```
276
390
 
277
391
  </details>
392
+
393
+ ### Locale badge visibility
394
+
395
+ With localized status badges enabled, the optional top-level
396
+ `localeBadgeVisibility: ({ doc, locale, req }) => boolean` callback decides which
397
+ badges are visible. It runs only on the server, against the latest draft with all
398
+ locale values and the current user's read access. Returning false hides the badge
399
+ from both sight and assistive technology while preserving its aligned slot,
400
+ including when that locale is selected. Publication status is unchanged.
401
+
402
+ Without a callback, all locale badges remain visible. When configured, the existing
403
+ batched draft query reads the document content needed by the callback; no per-row
404
+ queries are added. The callback and document content are not passed to the client.
405
+ Project-specific inheritance or detachment rules belong in the consuming app.
406
+
407
+ The optional top-level
408
+ `localeBadgeStatus: ({ doc, publishedDoc, locale, req, status }) => status`
409
+ callback overrides the displayed status. It receives the latest draft, the current
410
+ document (when readable), and the default computed status. Both documents contain
411
+ all locales. Check the current document's locale status before treating its content
412
+ as published. This callback loads content in the same two access-controlled batched
413
+ queries; no document content or callbacks are passed to the client. It never writes
414
+ publication state. Without it, the plugin's normal status behavior is unchanged.
@@ -10,9 +10,9 @@ 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 { PageTreeStatusBadge } from './PageTreeStatusBadge.js';
14
13
  import { PageTreeProvider, PageTreeRowDndProvider } from './PageTreeContext.js';
15
14
  import styles from './PageTreeListView.module.css';
15
+ import { PageTreeStatusBadge } from './PageTreeStatusBadge.js';
16
16
  import { PageTreeTitleCell } from './PageTreeTitleCell.js';
17
17
  const SILENT_MOVE_MESSAGES = new Set([
18
18
  CANCEL_DRAG_MESSAGE
@@ -194,7 +194,8 @@ function sliceColumnState(columnState, docs, renderedCellIndexByDocID) {
194
194
  function getSelectableRowData(doc) {
195
195
  const record = doc;
196
196
  return {
197
- id: String(doc.id ?? doc.__pageTreeID),
197
+ // Payload's selection Map preserves numeric IDs despite SelectRow's string-only type.
198
+ id: doc.id ?? doc.__pageTreeID,
198
199
  _isLocked: Boolean(record._isLocked),
199
200
  _userEditing: record._userEditing
200
201
  };
@@ -388,10 +389,11 @@ function getDropTargetValidation(args) {
388
389
  };
389
390
  }
390
391
  function renderStatusBadge(args) {
391
- const { badgeConfig, badgesLinks, doc, index } = args;
392
+ const { badgeConfig, badgesLinks, collectionSlug, doc, index } = args;
392
393
  return /*#__PURE__*/ _jsx(PageTreeStatusBadge, {
393
394
  badgeConfig: badgeConfig,
394
395
  badgesLinks: badgesLinks,
396
+ collectionSlug: collectionSlug,
395
397
  doc: doc
396
398
  }, doc.__pageTreeID ?? index);
397
399
  }
@@ -425,7 +427,7 @@ function ParentMoveToggle({ collectionSlug, enabled, onToggle }) {
425
427
  return toggle;
426
428
  }
427
429
  function buildTableColumns(args) {
428
- const { badgeConfig, badgesLinks, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
430
+ const { badgeConfig, badgesLinks, collectionSlug, columnState, docs, enableRowSelections, homeIndicatorEnabled, orderableFieldName, parentFieldSlug, t, useAsTitle } = args;
429
431
  const columnsToUse = columnState.map((column)=>{
430
432
  if (column.accessor === useAsTitle) {
431
433
  return {
@@ -444,6 +446,7 @@ function buildTableColumns(args) {
444
446
  renderedCells: docs.map((doc, index)=>renderStatusBadge({
445
447
  badgeConfig,
446
448
  badgesLinks,
449
+ collectionSlug,
447
450
  doc,
448
451
  index,
449
452
  t
@@ -892,6 +895,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, badgesLin
892
895
  const tableColumns = React.useMemo(()=>buildTableColumns({
893
896
  badgeConfig,
894
897
  badgesLinks,
898
+ collectionSlug: props.collectionSlug,
895
899
  columnState: paginatedColumnState,
896
900
  docs: displayedPaginatedDocs,
897
901
  enableRowSelections: props.enableRowSelections,
@@ -908,6 +912,7 @@ export default function PageTreeListViewClient({ allDocs, badgeConfig, badgesLin
908
912
  homeIndicatorEnabled,
909
913
  orderableFieldName,
910
914
  parentFieldSlug,
915
+ props.collectionSlug,
911
916
  props.enableRowSelections,
912
917
  i18n.t,
913
918
  useAsTitle